diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 69c71154..27f5e417 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -2,7 +2,7 @@ name: Swift on: push: - branches: [ feature/** ] + branches: [ feature/**, issue/**, test/** ] pull_request: branches: [ develop, feature/** ] @@ -11,10 +11,44 @@ jobs: runs-on: macos-latest env: - API_ACCESS_KEY: ${{ secrets.API_ACCESS_KEY }} + API_ACCESS_KEY1: ${{ secrets.API_ACCESS_KEY }} + API_ACCESS_KEY2: ${{ secrets.API_ACCESS_KEY2 }} + API_ACCESS_KEY3: ${{ secrets.API_ACCESS_KEY3 }} + API_ACCESS_KEY4: ${{ secrets.API_ACCESS_KEY4 }} + API_ACCESS_KEY5: ${{ secrets.API_ACCESS_KEY5 }} + API_ACCESS_KEY6: ${{ secrets.API_ACCESS_KEY6 }} + API_ACCESS_KEY7: ${{ secrets.API_ACCESS_KEY7 }} + API_ACCESS_KEY8: ${{ secrets.API_ACCESS_KEY8 }} + API_ACCESS_KEY9: ${{ secrets.API_ACCESS_KEY9 }} + API_ACCESS_KEY10: ${{ secrets.API_ACCESS_KEY10 }} + API_ACCESS_KEY11: ${{ secrets.API_ACCESS_KEY11 }} + API_ACCESS_KEY12: ${{ secrets.API_ACCESS_KEY12 }} + API_ACCESS_KEY13: ${{ secrets.API_ACCESS_KEY13 }} + API_ACCESS_KEY14: ${{ secrets.API_ACCESS_KEY14 }} + API_ACCESS_KEY15: ${{ secrets.API_ACCESS_KEY15 }} + API_ACCESS_KEY16: ${{ secrets.API_ACCESS_KEY16 }} + API_ACCESS_KEY17: ${{ secrets.API_ACCESS_KEY17 }} + steps: - uses: actions/checkout@v2 - name: Build run: | - xcodebuild test -project BBus/BBus.xcodeproj -scheme BBus -destination 'platform=iOS Simulator,name=iPhone 12,OS=latest' API_ACCESS_KEY=$API_ACCESS_KEY + xcodebuild test -project BBus/BBus.xcodeproj -scheme BBus -destination 'platform=iOS Simulator,name=iPhone 12,OS=latest' \ + API_ACCESS_KEY1=$API_ACCESS_KEY1 \ + API_ACCESS_KEY2=$API_ACCESS_KEY2 \ + API_ACCESS_KEY3=$API_ACCESS_KEY3 \ + API_ACCESS_KEY4=$API_ACCESS_KEY4 \ + API_ACCESS_KEY5=$API_ACCESS_KEY5 \ + API_ACCESS_KEY6=$API_ACCESS_KEY6 \ + API_ACCESS_KEY7=$API_ACCESS_KEY7 \ + API_ACCESS_KEY8=$API_ACCESS_KEY8 \ + API_ACCESS_KEY9=$API_ACCESS_KEY9 \ + API_ACCESS_KEY10=$API_ACCESS_KEY10 \ + API_ACCESS_KEY11=$API_ACCESS_KEY11 \ + API_ACCESS_KEY12=$API_ACCESS_KEY12 \ + API_ACCESS_KEY13=$API_ACCESS_KEY13 \ + API_ACCESS_KEY14=$API_ACCESS_KEY14 \ + API_ACCESS_KEY15=$API_ACCESS_KEY15 \ + API_ACCESS_KEY16=$API_ACCESS_KEY16 \ + API_ACCESS_KEY17=$API_ACCESS_KEY17 diff --git a/BBus/AlarmSettingViewModelTests/AlarmSettingViewModelTests.swift b/BBus/AlarmSettingViewModelTests/AlarmSettingViewModelTests.swift new file mode 100644 index 00000000..ee249b41 --- /dev/null +++ b/BBus/AlarmSettingViewModelTests/AlarmSettingViewModelTests.swift @@ -0,0 +1,156 @@ +// +// AlarmSettingViewModelTests.swift +// AlarmSettingViewModelTests +// +// Created by 김태훈 on 2021/11/30. +// + +import XCTest +import Combine + +class AlarmSettingViewModelTests: XCTestCase { + enum MOCKMode { + case success, failure + } + + enum TestError: Error { + case fail, jsonError + } + + class MOCKAlarmSettingAPIUseCase: AlarmSettingAPIUsable { + var mode: MOCKMode + var arrInfoByRouteDTO: ArrInfoByRouteDTO + + init(mode: MOCKMode, arrInfoByRouteDTO: ArrInfoByRouteDTO) { + self.mode = mode + self.arrInfoByRouteDTO = arrInfoByRouteDTO + } + + func busArriveInfoWillLoaded(stId: String, busRouteId: String, ord: String) -> AnyPublisher { + switch mode { + case .success: + return Just(self.arrInfoByRouteDTO) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + case .failure: + return Fail(error: TestError.fail).eraseToAnyPublisher() + } + + } + + func busStationsInfoWillLoaded(busRouetId: String, arsId: String) -> AnyPublisher<[StationByRouteListDTO]?, Error> { + return Just([]) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + } + + private var cancellables: Set! + private var arrInfoByRouteDTO: ArrInfoByRouteDTO! + private let firstArriveInfo = AlarmSettingBusArriveInfo(busArriveRemainTime: "곧 도착", + congestion: 3, + currentStation: "모두의학교.금천문화예술정보학교", + plainNumber: "서울71사1535", + vehicleId: 117020066) + private let secondArriveInfo = AlarmSettingBusArriveInfo(busArriveRemainTime: "7분9초후[3번째 전]", + congestion: 4, + currentStation: "호림박물관", + plainNumber: "서울71사1519", + vehicleId: 117020207) + + override func setUpWithError() throws { + guard let url = Bundle.init(identifier: "com.boostcamp.ios-009.AlarmSettingViewModelTests")? + .url(forResource: "MOCKArrInfo", withExtension: "json"), + let data = try? Data(contentsOf: url), + let arrInfoByRouteDTO = try? JSONDecoder().decode(ArrInfoByRouteDTO.self, from: data) else { throw TestError.jsonError } + + self.cancellables = [] + self.arrInfoByRouteDTO = arrInfoByRouteDTO + super.setUp() + } + + override func tearDownWithError() throws { + self.cancellables = nil + self.arrInfoByRouteDTO = nil + super.tearDown() + } + + func test_refresh_성공() { + let MOCCKAlarmSettingUseCase = MOCKAlarmSettingAPIUseCase(mode: .success, arrInfoByRouteDTO: self.arrInfoByRouteDTO) + let alarmSettingVieWModel = AlarmSettingViewModel(apiUseCase: MOCCKAlarmSettingUseCase, + calculateUseCase: AlarmSettingCalculateUseCase(), + stationId: 1, + busRouteId: 1, + stationOrd: 1, + arsId: "11111", + routeType: RouteType.mainLine, + busName: "11") + let expectation = self.expectation(description: "AlarmSettingViewModel에 busArriveInfos가 저장되는지 확인") + let expectedFirstArriveInfo = self.firstArriveInfo + let expectedSecondArriveInfo = self.secondArriveInfo + let expectedResult = AlarmSettingBusArriveInfos(arriveInfos: [expectedFirstArriveInfo, expectedSecondArriveInfo], changedByTimer: false) + + alarmSettingVieWModel.refresh() + alarmSettingVieWModel.$busArriveInfos + .receive(on: DispatchQueue.global()) + .filter { $0.count != 0 } + .sink { busArriveInfos in + let firstArriveInfo = busArriveInfos.arriveInfos[0] + let secondArriveInfo = busArriveInfos.arriveInfos[1] + + XCTAssertEqual(busArriveInfos.count, expectedResult.count) + XCTAssertEqual(firstArriveInfo.congestion, expectedFirstArriveInfo.congestion) + XCTAssertEqual(firstArriveInfo.arriveRemainTime?.message, expectedFirstArriveInfo.arriveRemainTime?.message) + XCTAssertEqual(firstArriveInfo.arriveRemainTime?.seconds, expectedFirstArriveInfo.arriveRemainTime?.seconds) + XCTAssertEqual(firstArriveInfo.currentStation, expectedFirstArriveInfo.currentStation) + XCTAssertEqual(firstArriveInfo.estimatedArrivalTime, expectedFirstArriveInfo.estimatedArrivalTime) + XCTAssertEqual(firstArriveInfo.relativePosition, expectedFirstArriveInfo.relativePosition) + XCTAssertEqual(firstArriveInfo.vehicleId, expectedFirstArriveInfo.vehicleId) + XCTAssertEqual(firstArriveInfo.plainNumber, expectedFirstArriveInfo.plainNumber) + XCTAssertEqual(secondArriveInfo.congestion, expectedSecondArriveInfo.congestion) + XCTAssertEqual(secondArriveInfo.arriveRemainTime?.message, expectedSecondArriveInfo.arriveRemainTime?.message) + XCTAssertEqual(secondArriveInfo.arriveRemainTime?.seconds, expectedSecondArriveInfo.arriveRemainTime?.seconds) + XCTAssertEqual(secondArriveInfo.currentStation, expectedSecondArriveInfo.currentStation) + XCTAssertEqual(secondArriveInfo.estimatedArrivalTime, expectedSecondArriveInfo.estimatedArrivalTime) + XCTAssertEqual(secondArriveInfo.relativePosition, expectedSecondArriveInfo.relativePosition) + XCTAssertEqual(secondArriveInfo.vehicleId, expectedSecondArriveInfo.vehicleId) + XCTAssertEqual(secondArriveInfo.plainNumber, expectedSecondArriveInfo.plainNumber) + XCTAssertFalse(busArriveInfos.changedByTimer) + + expectation.fulfill() + } + .store(in: &self.cancellables) + + waitForExpectations(timeout: 10) + } + + func test_refresh_arriveInfo가_에러를_리턴하여_실패() { + let MOCCKAlarmSettingUseCase = MOCKAlarmSettingAPIUseCase(mode: .failure, arrInfoByRouteDTO: self.arrInfoByRouteDTO) + let alarmSettingVieWModel = AlarmSettingViewModel(apiUseCase: MOCCKAlarmSettingUseCase, + calculateUseCase: AlarmSettingCalculateUseCase(), + stationId: 1, + busRouteId: 1, + stationOrd: 1, + arsId: "11111", + routeType: RouteType.mainLine, + busName: "11") + let expectation = self.expectation(description: "AlarmSettingViewModel에 busArriveInfos가 에러를 리턴하는지 확인") + + alarmSettingVieWModel.refresh() + alarmSettingVieWModel.$networkError + .receive(on: DispatchQueue.global()) + .compactMap { $0 } + .sink { error in + guard let error = error as? TestError else { XCTFail(); return; } + switch error { + case .fail: + expectation.fulfill() + default: + XCTFail() + } + } + .store(in: &self.cancellables) + + waitForExpectations(timeout: 10) + } +} diff --git a/BBus/AlarmSettingViewModelTests/MOCKArrInfo.json b/BBus/AlarmSettingViewModelTests/MOCKArrInfo.json new file mode 100644 index 00000000..c612df10 --- /dev/null +++ b/BBus/AlarmSettingViewModelTests/MOCKArrInfo.json @@ -0,0 +1,12 @@ +{ + "arrmsg1": "곧 도착", + "arrmsg2": "7분9초후[3번째 전]", + "reride_Num1": "3", + "reride_Num2": "4", + "stationNm1": "모두의학교.금천문화예술정보학교", + "stationNm2": "호림박물관", + "plainNo1": "서울71사1535", + "plainNo2": "서울71사1519", + "vehId1": "117020066", + "vehId2": "117020207" +} diff --git a/BBus/BBus.xcodeproj/project.pbxproj b/BBus/BBus.xcodeproj/project.pbxproj index 3dd9d64d..bd6c03ce 100644 --- a/BBus/BBus.xcodeproj/project.pbxproj +++ b/BBus/BBus.xcodeproj/project.pbxproj @@ -11,6 +11,35 @@ 04045B812740F6DC0056A433 /* BusSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04045B802740F6DC0056A433 /* BusSearchResult.swift */; }; 041A5A11273D2E1B00490075 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5A10273D2E1B00490075 /* StringExtension.swift */; }; 04214061273A5EBC00A15423 /* MovingStatusFoldUnfoldDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04214060273A5EBC00A15423 /* MovingStatusFoldUnfoldDelegate.swift */; }; + 0446840C275650B0007E440A /* TokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182A32754E3CA001EA530 /* TokenManager.swift */; }; + 0446840D2756597F007E440A /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; + 04468427275675F1007E440A /* SearchAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468426275675F1007E440A /* SearchAPIUsable.swift */; }; + 044684292756760A007E440A /* SearchCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044684282756760A007E440A /* SearchCalculatable.swift */; }; + 0446842B27567654007E440A /* HomeAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446842A27567654007E440A /* HomeAPIUsable.swift */; }; + 0446842D27567660007E440A /* HomeCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446842C27567660007E440A /* HomeCalculatable.swift */; }; + 0446842F275676B3007E440A /* BusRouteAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446842E275676B3007E440A /* BusRouteAPIUsable.swift */; }; + 04468431275676ED007E440A /* StationAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468430275676ED007E440A /* StationAPIUsable.swift */; }; + 04468433275676F8007E440A /* StationCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468432275676F8007E440A /* StationCalculatable.swift */; }; + 0446843527567769007E440A /* AlarmSettingAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843427567769007E440A /* AlarmSettingAPIUsable.swift */; }; + 0446843727567777007E440A /* AlarmSettingCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843627567777007E440A /* AlarmSettingCalculatable.swift */; }; + 04468439275677C0007E440A /* MovingStatusAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468438275677C0007E440A /* MovingStatusAPIUsable.swift */; }; + 0446843B275677CA007E440A /* MovingStatusCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843A275677CA007E440A /* MovingStatusCalculatable.swift */; }; + 0446843D275679B9007E440A /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; }; + 0446843E27567F4E007E440A /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51F2272FCDF900EC0531 /* SearchViewModel.swift */; }; + 0446843F27568065007E440A /* SearchAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468426275675F1007E440A /* SearchAPIUsable.swift */; }; + 0446844027568075007E440A /* SearchCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044684282756760A007E440A /* SearchCalculatable.swift */; }; + 0446844127568084007E440A /* BusRouteDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AA273B9F330061ACDA /* BusRouteDTO.swift */; }; + 0446844227568091007E440A /* StationDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AE273BAA130061ACDA /* StationDTO.swift */; }; + 0446844327568096007E440A /* SearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04045B7E2740F6B20056A433 /* SearchResults.swift */; }; + 04468444275680A0007E440A /* BusSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04045B802740F6DC0056A433 /* BusSearchResult.swift */; }; + 04468445275680B1007E440A /* BaseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100322754CDB100754B36 /* BaseUseCase.swift */; }; + 04468446275680FF007E440A /* StationSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51EE272FCDE600EC0531 /* StationSearchResult.swift */; }; + 0446844727568119007E440A /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; }; + 044684482756812D007E440A /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; }; + 0446844927568134007E440A /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; + 0446844A27568A3B007E440A /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5A10273D2E1B00490075 /* StringExtension.swift */; }; + 0446844D275694D7007E440A /* SearchCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0451EDB32755BBBD00031A16 /* SearchCalculateUseCase.swift */; }; + 0451EDB42755BBBD00031A16 /* SearchCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0451EDB32755BBBD00031A16 /* SearchCalculateUseCase.swift */; }; 0476ABBB27310ED200F72DD1 /* BusRouteHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0476ABBA27310ED200F72DD1 /* BusRouteHeaderView.swift */; }; 0476ABBD27311C1600F72DD1 /* BusStationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0476ABBC27311C1600F72DD1 /* BusStationTableViewCell.swift */; }; 047C30FA273A04B40075EA14 /* BBusImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047C30F9273A04B40075EA14 /* BBusImage.swift */; }; @@ -31,6 +60,25 @@ 04AC7D1B2733E7270095CD4E /* AlarmSettingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AC7D1A2733E7270095CD4E /* AlarmSettingButton.swift */; }; 04C6D6692734B18A00D41678 /* MovingStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C6D6682734B18A00D41678 /* MovingStatusTableViewCell.swift */; }; 04C6D66B2734BCAB00D41678 /* MovingStatusBusTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C6D66A2734BCAB00D41678 /* MovingStatusBusTagView.swift */; }; + 04DC47FB27552FE5003380D9 /* StationCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DC47FA27552FE5003380D9 /* StationCalculateUseCase.swift */; }; + 04DEBDEC27572E4700B53D5F /* AlarmSettingAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843427567769007E440A /* AlarmSettingAPIUsable.swift */; }; + 04DEBDED27572E6400B53D5F /* ArrInfoByRouteDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375BC273C2E120061ACDA /* ArrInfoByRouteDTO.swift */; }; + 04DEBDEE27572E8400B53D5F /* BaseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100322754CDB100754B36 /* BaseUseCase.swift */; }; + 04DEBDEF27572E8800B53D5F /* StationByRouteListDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375B4273BB98E0061ACDA /* StationByRouteListDTO.swift */; }; + 04DEBDF027572E8D00B53D5F /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; }; + 04DEBDF22757311F00B53D5F /* MOCKArrInfo.json in Resources */ = {isa = PBXBuildFile; fileRef = 04DEBDF12757311F00B53D5F /* MOCKArrInfo.json */; }; + 04DEBDF32757353800B53D5F /* AlarmSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5222272FCEB500EC0531 /* AlarmSettingViewModel.swift */; }; + 04DEBDF42757355D00B53D5F /* BusRouteDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AA273B9F330061ACDA /* BusRouteDTO.swift */; }; + 04DEBDF5275735D900B53D5F /* AlarmSettingCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875F1AB12755BE08003F5BB1 /* AlarmSettingCalculateUseCase.swift */; }; + 04DEBDF62757364D00B53D5F /* AlarmSettingCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843627567777007E440A /* AlarmSettingCalculatable.swift */; }; + 04DEBDF72757365C00B53D5F /* AverageSectionTimeCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875F1AAF2755BD86003F5BB1 /* AverageSectionTimeCalculatable.swift */; }; + 04DEBDF82757366C00B53D5F /* AlarmSettingBusArriveInfos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484107527464D49006F8636 /* AlarmSettingBusArriveInfos.swift */; }; + 04DEBDF9275736AB00B53D5F /* NotificationNameExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A06AEE82743DAB20027222D /* NotificationNameExtension.swift */; }; + 04DEBDFA275736B500B53D5F /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; }; + 04DEBDFB275736C200B53D5F /* AlarmSettingBusArriveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA521E272FCEAA00EC0531 /* AlarmSettingBusArriveInfo.swift */; }; + 04DEBDFC275736D700B53D5F /* BusRemainTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A094CDE27435C5900428F55 /* BusRemainTime.swift */; }; + 04DEBDFD275736DE00B53D5F /* BusCongestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5212272FCE8500EC0531 /* BusCongestion.swift */; }; + 04DEBDFE275736EE00B53D5F /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; 4A04682427327876008D87CE /* BusRouteCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A04682327327876008D87CE /* BusRouteCoordinator.swift */; }; 4A04682627327BA0008D87CE /* AlarmSettingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A04682527327BA0008D87CE /* AlarmSettingCoordinator.swift */; }; 4A04682A2732B7B3008D87CE /* SearchNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0468292732B7B3008D87CE /* SearchNavigationView.swift */; }; @@ -45,9 +93,51 @@ 4A1A22DB27326FD100476861 /* HomeNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1A22DA27326FD100476861 /* HomeNavigationView.swift */; }; 4A1A22DD2732801700476861 /* StationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1A22DC2732801700476861 /* StationCoordinator.swift */; }; 4A1A22E12732CE7900476861 /* SearchResultCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1A22E02732CE7900476861 /* SearchResultCollectionViewCell.swift */; }; + 4A2634AA2756674900267B47 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A89273B96B60078EAE3 /* NetworkService.swift */; }; + 4A2634AB2756678100267B47 /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; + 4A2634AC27566F8600267B47 /* StationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5216272FCE8E00EC0531 /* StationViewModel.swift */; }; + 4A2634AF275670A500267B47 /* BaseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100322754CDB100754B36 /* BaseUseCase.swift */; }; + 4A2634B0275670BE00267B47 /* StationByUidItemDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375B8273BE2640061ACDA /* StationByUidItemDTO.swift */; }; + 4A2634B1275670C500267B47 /* FavoriteItemDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A06AEC7274159D10027222D /* FavoriteItemDTO.swift */; }; + 4A2634B2275670CB00267B47 /* BusRouteDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AA273B9F330061ACDA /* BusRouteDTO.swift */; }; + 4A2634B3275670D100267B47 /* StationDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AE273BAA130061ACDA /* StationDTO.swift */; }; + 4A2634B62756754E00267B47 /* BusSectionKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACC26B3274F669800173A32 /* BusSectionKeys.swift */; }; + 4A2634B72756755700267B47 /* BBusRouteType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047C30FD273A05A60075EA14 /* BBusRouteType.swift */; }; + 4A2634B82756756200267B47 /* BusArriveInfos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACC26B7274F6BD300173A32 /* BusArriveInfos.swift */; }; + 4A2634B92756757400267B47 /* NotificationNameExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A06AEE82743DAB20027222D /* NotificationNameExtension.swift */; }; + 4A2634BA2756757F00267B47 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; }; + 4A2634BB2756758C00267B47 /* BusCongestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5212272FCE8500EC0531 /* BusCongestion.swift */; }; + 4A2634BC2756759700267B47 /* AlarmSettingBusArriveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA521E272FCEAA00EC0531 /* AlarmSettingBusArriveInfo.swift */; }; + 4A2634BE275675C400267B47 /* BusRemainTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A094CDE27435C5900428F55 /* BusRemainTime.swift */; }; + 4A2634BF275675CD00267B47 /* BusPosByVehicleIdDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29BA274B6C8300554A4E /* BusPosByVehicleIdDTO.swift */; }; + 4A2634C12756862F00267B47 /* StationAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468430275676ED007E440A /* StationAPIUsable.swift */; }; + 4A2634C22756863600267B47 /* StationCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468432275676F8007E440A /* StationCalculatable.swift */; }; + 4A2634C32756864A00267B47 /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; }; + 4A2634C427568CB900267B47 /* StationCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DC47FA27552FE5003380D9 /* StationCalculateUseCase.swift */; }; + 4A2634C5275693BC00267B47 /* DummyJsonStringStationByUidItemDTO.json in Resources */ = {isa = PBXBuildFile; fileRef = 4A2634C027567E5400267B47 /* DummyJsonStringStationByUidItemDTO.json */; }; 4A38E20727463FF7003A9D10 /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; 4A38E20927464015003A9D10 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; }; + 4A3B6C502755DF2400BBC498 /* AlarmCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3B6C4F2755DF2400BBC498 /* AlarmCenter.swift */; }; 4A412028274E34430083D691 /* BusRouteModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A412027274E34430083D691 /* BusRouteModel.swift */; }; + 4A5100332754CDB100754B36 /* BaseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100322754CDB100754B36 /* BaseUseCase.swift */; }; + 4A5100352754CE1500754B36 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100342754CE1500754B36 /* BaseViewController.swift */; }; + 4A5100372755106A00754B36 /* HomeCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100362755106A00754B36 /* HomeCalculateUseCase.swift */; }; + 4A510039275517F800754B36 /* RefreshableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A510038275517F800754B36 /* RefreshableView.swift */; }; + 4A51003B2755180400754B36 /* RefreshButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A51003A2755180400754B36 /* RefreshButton.swift */; }; + 4A5182A42754E3CA001EA530 /* TokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182A32754E3CA001EA530 /* TokenManager.swift */; }; + 4A5182A627550988001EA530 /* RequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182A527550988001EA530 /* RequestFactory.swift */; }; + 4A5182A8275511D5001EA530 /* ServiceFetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182A7275511D5001EA530 /* ServiceFetchable.swift */; }; + 4A5182AC275516A4001EA530 /* PersistencetFetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182AB275516A4001EA530 /* PersistencetFetchable.swift */; }; + 4A5182AF27551E3C001EA530 /* GetArrInfoByRouteListUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182AE27551E3C001EA530 /* GetArrInfoByRouteListUsable.swift */; }; + 4A5182B127551E84001EA530 /* GetStationsByRouteListUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182B027551E84001EA530 /* GetStationsByRouteListUsable.swift */; }; + 4A5182B327551EA9001EA530 /* GetBusPosByRtidUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182B227551EA9001EA530 /* GetBusPosByRtidUsable.swift */; }; + 4A5182B527551EC2001EA530 /* GetStationByUidItemUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182B427551EC2001EA530 /* GetStationByUidItemUsable.swift */; }; + 4A5182B727551ED8001EA530 /* GetRouteListUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182B627551ED8001EA530 /* GetRouteListUsable.swift */; }; + 4A5182B927551F06001EA530 /* GetBusPosByVehIdUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182B827551F06001EA530 /* GetBusPosByVehIdUsable.swift */; }; + 4A5182BB27551F28001EA530 /* GetStationListUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182BA27551F28001EA530 /* GetStationListUsable.swift */; }; + 4A5182BD27551F40001EA530 /* GetFavoriteItemListUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182BC27551F40001EA530 /* GetFavoriteItemListUsable.swift */; }; + 4A5182BF27551F57001EA530 /* CreateFavoriteItemUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182BE27551F57001EA530 /* CreateFavoriteItemUsable.swift */; }; + 4A5182C127551F6D001EA530 /* DeleteFavoriteItemUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182C027551F6D001EA530 /* DeleteFavoriteItemUsable.swift */; }; 4A7BBFED2737885F0029915F /* StationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A7BBFEC2737885F0029915F /* StationHeaderView.swift */; }; 4A7BBFEF27378AA50029915F /* StationBodyCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A7BBFEE27378AA50029915F /* StationBodyCollectionViewCell.swift */; }; 4A7BBFF12737D6C20029915F /* RemainCongestionBadgeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A7BBFF02737D6C20029915F /* RemainCongestionBadgeLabel.swift */; }; @@ -64,26 +154,26 @@ 4AA294B7273C1275008E5497 /* DeleteFavoriteItemFetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA294B6273C1275008E5497 /* DeleteFavoriteItemFetchable.swift */; }; 4AC79161274F6FDB00019827 /* SourceFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC79160274F6FDB00019827 /* SourceFooterView.swift */; }; 4ACA51E3272FCD9600EC0531 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51E2272FCD9600EC0531 /* HomeModel.swift */; }; - 4ACA51E5272FCD9C00EC0531 /* HomeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51E4272FCD9C00EC0531 /* HomeUseCase.swift */; }; + 4ACA51E5272FCD9C00EC0531 /* HomeAPIUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51E4272FCD9C00EC0531 /* HomeAPIUseCase.swift */; }; 4ACA51E7272FCDA600EC0531 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51E6272FCDA600EC0531 /* HomeViewModel.swift */; }; 4ACA51E9272FCDAE00EC0531 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51E8272FCDAE00EC0531 /* HomeView.swift */; }; 4ACA51EF272FCDE600EC0531 /* StationSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51EE272FCDE600EC0531 /* StationSearchResult.swift */; }; - 4ACA51F1272FCDF200EC0531 /* SearchUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51F0272FCDF200EC0531 /* SearchUseCase.swift */; }; + 4ACA51F1272FCDF200EC0531 /* SearchAPIUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51F0272FCDF200EC0531 /* SearchAPIUseCase.swift */; }; 4ACA51F3272FCDF900EC0531 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51F2272FCDF900EC0531 /* SearchViewModel.swift */; }; 4ACA51F5272FCE0200EC0531 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA51F4272FCE0200EC0531 /* SearchView.swift */; }; - 4ACA5209272FCE5A00EC0531 /* BusRouteUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5208272FCE5A00EC0531 /* BusRouteUseCase.swift */; }; + 4ACA5209272FCE5A00EC0531 /* BusRouteAPIUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5208272FCE5A00EC0531 /* BusRouteAPIUseCase.swift */; }; 4ACA520B272FCE5F00EC0531 /* BusRouteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA520A272FCE5F00EC0531 /* BusRouteViewModel.swift */; }; 4ACA520D272FCE6500EC0531 /* BusRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA520C272FCE6500EC0531 /* BusRouteView.swift */; }; 4ACA5213272FCE8500EC0531 /* BusCongestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5212272FCE8500EC0531 /* BusCongestion.swift */; }; - 4ACA5215272FCE8A00EC0531 /* StationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5214272FCE8A00EC0531 /* StationUseCase.swift */; }; + 4ACA5215272FCE8A00EC0531 /* StationAPIUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5214272FCE8A00EC0531 /* StationAPIUseCase.swift */; }; 4ACA5217272FCE8E00EC0531 /* StationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5216272FCE8E00EC0531 /* StationViewModel.swift */; }; 4ACA5219272FCE9500EC0531 /* StationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5218272FCE9500EC0531 /* StationView.swift */; }; 4ACA521F272FCEAA00EC0531 /* AlarmSettingBusArriveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA521E272FCEAA00EC0531 /* AlarmSettingBusArriveInfo.swift */; }; - 4ACA5221272FCEAF00EC0531 /* AlarmSettingUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5220272FCEAF00EC0531 /* AlarmSettingUseCase.swift */; }; + 4ACA5221272FCEAF00EC0531 /* AlarmSettingAPIUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5220272FCEAF00EC0531 /* AlarmSettingAPIUseCase.swift */; }; 4ACA5223272FCEB500EC0531 /* AlarmSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5222272FCEB500EC0531 /* AlarmSettingViewModel.swift */; }; 4ACA5225272FCEBF00EC0531 /* AlarmSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5224272FCEBF00EC0531 /* AlarmSettingView.swift */; }; 4ACA522B272FCED600EC0531 /* MovingStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA522A272FCED600EC0531 /* MovingStatusModel.swift */; }; - 4ACA522D272FCEDB00EC0531 /* MovingStatusUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA522C272FCEDB00EC0531 /* MovingStatusUseCase.swift */; }; + 4ACA522D272FCEDB00EC0531 /* MovingStatusAPIUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA522C272FCEDB00EC0531 /* MovingStatusAPIUseCase.swift */; }; 4ACA522F272FCEDF00EC0531 /* MovingStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA522E272FCEDF00EC0531 /* MovingStatusViewModel.swift */; }; 4ACA5231272FCEE600EC0531 /* MovingStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5230272FCEE600EC0531 /* MovingStatusView.swift */; }; 4ACA5233272FCF0F00EC0531 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5232272FCF0F00EC0531 /* SearchViewController.swift */; }; @@ -98,23 +188,87 @@ 4ADB29BB274B6C8300554A4E /* BusPosByVehicleIdDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29BA274B6C8300554A4E /* BusPosByVehicleIdDTO.swift */; }; 4ADB29BD274B6E0B00554A4E /* GetBusPosByVehIdFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29BC274B6E0B00554A4E /* GetBusPosByVehIdFetcher.swift */; }; 4ADB29C7274B7A7F00554A4E /* GetOnAlarmStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29C6274B7A7F00554A4E /* GetOnAlarmStatus.swift */; }; - 4ADB29C9274B7AA200554A4E /* GetOnAlarmUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29C8274B7AA200554A4E /* GetOnAlarmUsecase.swift */; }; + 4ADB29C9274B7AA200554A4E /* GetOnAlarmAPIUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29C8274B7AA200554A4E /* GetOnAlarmAPIUseCase.swift */; }; 4ADB29CB274B7AB900554A4E /* GetOnAlarmViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29CA274B7AB900554A4E /* GetOnAlarmViewModel.swift */; }; - 4ADB29CD274B880C00554A4E /* BusApproachCheckUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29CC274B880C00554A4E /* BusApproachCheckUsecase.swift */; }; + 4ADB29CD274B880C00554A4E /* GetOnAlarmCalculateUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29CC274B880C00554A4E /* GetOnAlarmCalculateUsecase.swift */; }; 4ADB29CF274B890800554A4E /* BusApproachStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29CE274B890800554A4E /* BusApproachStatus.swift */; }; 4ADB29D1274CAF0100554A4E /* AlarmStartResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29D0274CAF0100554A4E /* AlarmStartResult.swift */; }; 4ADB29D4274E18BB00554A4E /* GetOffAlarmController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29D3274E18BB00554A4E /* GetOffAlarmController.swift */; }; 4ADB29D7274E198900554A4E /* GetOffAlarmViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29D6274E198900554A4E /* GetOffAlarmViewModel.swift */; }; 4ADB29DA274E1A5F00554A4E /* GetOffAlarmStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29D9274E1A5F00554A4E /* GetOffAlarmStatus.swift */; }; - 87038A82273B90A50078EAE3 /* BBusAPIUsecases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A81273B90A50078EAE3 /* BBusAPIUsecases.swift */; }; - 87038A85273B90D10078EAE3 /* RequestUsecases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A84273B90D10078EAE3 /* RequestUsecases.swift */; }; + 4AF1E0602755BE2400DE51C8 /* NavigatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E05F2755BE2400DE51C8 /* NavigatableView.swift */; }; + 4AF1E0682756262300DE51C8 /* NetworkServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E0672756262300DE51C8 /* NetworkServiceTests.swift */; }; + 4AF1E0752756263400DE51C8 /* PersistenceStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E0742756263400DE51C8 /* PersistenceStorageTests.swift */; }; + 4AF1E0822756263E00DE51C8 /* TokenManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E0812756263E00DE51C8 /* TokenManagerTests.swift */; }; + 4AF1E08F2756264600DE51C8 /* RequestFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E08E2756264600DE51C8 /* RequestFactoryTests.swift */; }; + 4AF1E09C2756266B00DE51C8 /* HomeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E09B2756266B00DE51C8 /* HomeViewModelTests.swift */; }; + 4AF1E0A92756268B00DE51C8 /* SearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E0A82756268B00DE51C8 /* SearchViewModelTests.swift */; }; + 4AF1E0B62756269600DE51C8 /* StationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E0B52756269600DE51C8 /* StationViewModelTests.swift */; }; + 4AF1E0C32756269F00DE51C8 /* BusRouteViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E0C22756269F00DE51C8 /* BusRouteViewModelTests.swift */; }; + 4AF1E0D0275626A700DE51C8 /* AlarmSettingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E0CF275626A700DE51C8 /* AlarmSettingViewModelTests.swift */; }; + 4AF1E0DD275626B300DE51C8 /* MovingStatusViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF1E0DC275626B300DE51C8 /* MovingStatusViewModelTests.swift */; }; + 4AF1E0E327566C8600DE51C8 /* PersistenceStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A8B273B96DF0078EAE3 /* PersistenceStorage.swift */; }; + 4AF1E0E42756725900DE51C8 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; }; + 4AF1E0E62756734A00DE51C8 /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; + 4AF1E0E72756735B00DE51C8 /* FavoriteItemDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A06AEC7274159D10027222D /* FavoriteItemDTO.swift */; }; + 4AF1E0E827567DE100DE51C8 /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; }; + 87038A82273B90A50078EAE3 /* BBusAPIUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A81273B90A50078EAE3 /* BBusAPIUseCases.swift */; }; 87038A88273B950B0078EAE3 /* GetArrInfoByRouteListFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A87273B950B0078EAE3 /* GetArrInfoByRouteListFetcher.swift */; }; - 87038A8A273B96B60078EAE3 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A89273B96B60078EAE3 /* Service.swift */; }; - 87038A8C273B96DF0078EAE3 /* Persistent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A8B273B96DF0078EAE3 /* Persistent.swift */; }; + 87038A8A273B96B60078EAE3 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A89273B96B60078EAE3 /* NetworkService.swift */; }; + 87038A8C273B96DF0078EAE3 /* PersistenceStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A8B273B96DF0078EAE3 /* PersistenceStorage.swift */; }; 87038A8E273C10480078EAE3 /* GetRouteInfoItemFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A8D273C10480078EAE3 /* GetRouteInfoItemFetcher.swift */; }; 87038A90273C11630078EAE3 /* GetStationsByRouteListFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A8F273C11630078EAE3 /* GetStationsByRouteListFetcher.swift */; }; 87038A92273C12320078EAE3 /* GetBusPosByRtidFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A91273C12320078EAE3 /* GetBusPosByRtidFetcher.swift */; }; 87038A94273C12E20078EAE3 /* GetStationByUidItemFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A93273C12E20078EAE3 /* GetStationByUidItemFetcher.swift */; }; + 87075FFD27569066005A1E37 /* MovingStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA522E272FCEDF00EC0531 /* MovingStatusViewModel.swift */; }; + 87075FFE275690E0005A1E37 /* MovingStatusAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04468438275677C0007E440A /* MovingStatusAPIUsable.swift */; }; + 87075FFF2756914E005A1E37 /* BusRouteDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AA273B9F330061ACDA /* BusRouteDTO.swift */; }; + 8707600027569158005A1E37 /* StationByRouteListDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375B4273BB98E0061ACDA /* StationByRouteListDTO.swift */; }; + 8707600127569160005A1E37 /* BusPosByRtidDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375B6273BE08F0061ACDA /* BusPosByRtidDTO.swift */; }; + 8707600227569254005A1E37 /* MovingStatusCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843A275677CA007E440A /* MovingStatusCalculatable.swift */; }; + 8707600327569254005A1E37 /* MovingStatusCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B35D092754E71F00159791 /* MovingStatusCalculateUseCase.swift */; }; + 8707600527569303005A1E37 /* AverageSectionTimeCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875F1AAF2755BD86003F5BB1 /* AverageSectionTimeCalculatable.swift */; }; + 8707600627569309005A1E37 /* BaseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100322754CDB100754B36 /* BaseUseCase.swift */; }; + 8707600727569319005A1E37 /* NotificationNameExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A06AEE82743DAB20027222D /* NotificationNameExtension.swift */; }; + 8707600827569324005A1E37 /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; }; + 870760092756932E005A1E37 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; }; + 8707600A27569337005A1E37 /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; + 87115EFA27564D0F00601770 /* RequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182A527550988001EA530 /* RequestFactory.swift */; }; + 87115EFB27565A3300601770 /* BusRouteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA520A272FCE5F00EC0531 /* BusRouteViewModel.swift */; }; + 87115EFC27565BAD00601770 /* BusRouteAPIUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5208272FCE5A00EC0531 /* BusRouteAPIUseCase.swift */; }; + 87115EFD27565D9D00601770 /* BusRouteDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375AA273B9F330061ACDA /* BusRouteDTO.swift */; }; + 87115EFE27565DAE00601770 /* StationByRouteListDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375B4273BB98E0061ACDA /* StationByRouteListDTO.swift */; }; + 87115EFF27565DC200601770 /* BusPosByRtidDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049375B6273BE08F0061ACDA /* BusPosByRtidDTO.swift */; }; + 87115F00275667A600601770 /* BaseUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5100322754CDB100754B36 /* BaseUseCase.swift */; }; + 87115F01275668E100601770 /* BusCongestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACA5212272FCE8500EC0531 /* BusCongestion.swift */; }; + 87115F02275668F600601770 /* NotificationNameExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A06AEE82743DAB20027222D /* NotificationNameExtension.swift */; }; + 87115F032756693700601770 /* BusPosByVehicleIdDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ADB29BA274B6C8300554A4E /* BusPosByVehicleIdDTO.swift */; }; + 87115F042756694600601770 /* PublisherExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20827464015003A9D10 /* PublisherExtension.swift */; }; + 87115F052756696F00601770 /* GetRouteListUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182B627551ED8001EA530 /* GetRouteListUsable.swift */; }; + 87115F062756698D00601770 /* BBusAPIUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A81273B90A50078EAE3 /* BBusAPIUseCases.swift */; }; + 87115F0727566A4400601770 /* GetStationsByRouteListUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182B027551E84001EA530 /* GetStationsByRouteListUsable.swift */; }; + 87115F0827566A5200601770 /* TokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182A32754E3CA001EA530 /* TokenManager.swift */; }; + 87115F0927566A5900601770 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A89273B96B60078EAE3 /* NetworkService.swift */; }; + 87115F0A27566A6200601770 /* PersistenceStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A8B273B96DF0078EAE3 /* PersistenceStorage.swift */; }; + 87115F0B27566A6900601770 /* RequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182A527550988001EA530 /* RequestFactory.swift */; }; + 87115F0C27566A7000601770 /* GetStationsByRouteListFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A8F273C11630078EAE3 /* GetStationsByRouteListFetcher.swift */; }; + 87115F0D27566A7800601770 /* ServiceFetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182A7275511D5001EA530 /* ServiceFetchable.swift */; }; + 87115F0E27566A8000601770 /* GetBusPosByRtidUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182B227551EA9001EA530 /* GetBusPosByRtidUsable.swift */; }; + 87115F0F27566A8E00601770 /* GetBusPosByRtidFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87038A91273C12320078EAE3 /* GetBusPosByRtidFetcher.swift */; }; + 87115F1027566A9800601770 /* GetRouteListFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA294AE273C0E8D008E5497 /* GetRouteListFetcher.swift */; }; + 87115F1127566A9F00601770 /* BBusAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */; }; + 87115F1227566AA700601770 /* PersistencetFetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5182AB275516A4001EA530 /* PersistencetFetchable.swift */; }; + 87115F1327566AB200601770 /* FavoriteItemDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A06AEC7274159D10027222D /* FavoriteItemDTO.swift */; }; + 87115F172756758800601770 /* GetArrInfoByRouteListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F162756758800601770 /* GetArrInfoByRouteListUseCase.swift */; }; + 87115F19275675B900601770 /* GetStationsByRouteListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F18275675B900601770 /* GetStationsByRouteListUseCase.swift */; }; + 87115F1B275675E200601770 /* GetBusPosByRtidUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F1A275675E200601770 /* GetBusPosByRtidUseCase.swift */; }; + 87115F1D2756761200601770 /* GetStationByUidItemUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F1C2756761200601770 /* GetStationByUidItemUseCase.swift */; }; + 87115F1F2756766900601770 /* GetBusPosByVehIdUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F1E2756766900601770 /* GetBusPosByVehIdUseCase.swift */; }; + 87115F212756768900601770 /* GetRouteListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F202756768900601770 /* GetRouteListUseCase.swift */; }; + 87115F232756769D00601770 /* GetStationListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F222756769D00601770 /* GetStationListUseCase.swift */; }; + 87115F25275676B000601770 /* GetFavoriteItemListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F24275676B000601770 /* GetFavoriteItemListUseCase.swift */; }; + 87115F27275676C500601770 /* CreateFavoriteItemUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F26275676C500601770 /* CreateFavoriteItemUseCase.swift */; }; + 87115F29275676E900601770 /* DeleteFavoriteItemUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87115F28275676E900601770 /* DeleteFavoriteItemUseCase.swift */; }; 87285F5527461DB300CA3BA9 /* UINavigationControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87285F5427461DB300CA3BA9 /* UINavigationControllerExtension.swift */; }; 873578832732545D00CC8ECC /* CustomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873578822732545D00CC8ECC /* CustomNavigationBar.swift */; }; 87359B8D27311BF100F461A7 /* BusTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87359B8C27311BF100F461A7 /* BusTagView.swift */; }; @@ -122,11 +276,16 @@ 873D639827303A6800E79069 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873D639727303A6800E79069 /* AppCoordinator.swift */; }; 873D639A27303B0500E79069 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873D639927303B0500E79069 /* HomeCoordinator.swift */; }; 873D639C27303B5000E79069 /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873D639B27303B5000E79069 /* SearchCoordinator.swift */; }; + 875483782756810400136F16 /* BusRouteAPIUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446842E275676B3007E440A /* BusRouteAPIUsable.swift */; }; + 875483792756810D00136F16 /* JsonDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0446843C275679B9007E440A /* JsonDTO.swift */; }; + 875F1AB02755BD86003F5BB1 /* AverageSectionTimeCalculatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875F1AAF2755BD86003F5BB1 /* AverageSectionTimeCalculatable.swift */; }; + 875F1AB22755BE08003F5BB1 /* AlarmSettingCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875F1AB12755BE08003F5BB1 /* AlarmSettingCalculateUseCase.swift */; }; 87A5556C2728116400A9B5E3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A5556B2728116400A9B5E3 /* AppDelegate.swift */; }; 87A5556E2728116400A9B5E3 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A5556D2728116400A9B5E3 /* SceneDelegate.swift */; }; 87A555702728116400A9B5E3 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A5556F2728116400A9B5E3 /* HomeViewController.swift */; }; 87A555752728116600A9B5E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 87A555742728116600A9B5E3 /* Assets.xcassets */; }; 87A555782728116600A9B5E3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 87A555762728116600A9B5E3 /* LaunchScreen.storyboard */; }; + 87B35D0A2754E71F00159791 /* MovingStatusCalculateUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B35D092754E71F00159791 /* MovingStatusCalculateUseCase.swift */; }; 87EB52AE2733A6C6000D5492 /* GetOnStatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EB52AD2733A6C6000D5492 /* GetOnStatusCell.swift */; }; /* End PBXBuildFile section */ @@ -138,6 +297,76 @@ remoteGlobalIDString = 87A555672728116400A9B5E3; remoteInfo = BBus; }; + 4AF1E0692756262300DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; + 4AF1E0762756263400DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; + 4AF1E0832756263E00DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; + 4AF1E0902756264600DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; + 4AF1E09D2756266B00DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; + 4AF1E0AA2756268B00DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; + 4AF1E0B72756269600DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; + 4AF1E0C42756269F00DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; + 4AF1E0D1275626A700DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; + 4AF1E0DE275626B300DE51C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87A555602728116400A9B5E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87A555672728116400A9B5E3; + remoteInfo = BBus; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -145,6 +374,19 @@ 04045B802740F6DC0056A433 /* BusSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusSearchResult.swift; sourceTree = ""; }; 041A5A10273D2E1B00490075 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 04214060273A5EBC00A15423 /* MovingStatusFoldUnfoldDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusFoldUnfoldDelegate.swift; sourceTree = ""; }; + 04468426275675F1007E440A /* SearchAPIUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAPIUsable.swift; sourceTree = ""; }; + 044684282756760A007E440A /* SearchCalculatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCalculatable.swift; sourceTree = ""; }; + 0446842A27567654007E440A /* HomeAPIUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAPIUsable.swift; sourceTree = ""; }; + 0446842C27567660007E440A /* HomeCalculatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCalculatable.swift; sourceTree = ""; }; + 0446842E275676B3007E440A /* BusRouteAPIUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusRouteAPIUsable.swift; sourceTree = ""; }; + 04468430275676ED007E440A /* StationAPIUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationAPIUsable.swift; sourceTree = ""; }; + 04468432275676F8007E440A /* StationCalculatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationCalculatable.swift; sourceTree = ""; }; + 0446843427567769007E440A /* AlarmSettingAPIUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingAPIUsable.swift; sourceTree = ""; }; + 0446843627567777007E440A /* AlarmSettingCalculatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingCalculatable.swift; sourceTree = ""; }; + 04468438275677C0007E440A /* MovingStatusAPIUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusAPIUsable.swift; sourceTree = ""; }; + 0446843A275677CA007E440A /* MovingStatusCalculatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusCalculatable.swift; sourceTree = ""; }; + 0446843C275679B9007E440A /* JsonDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonDTO.swift; sourceTree = ""; }; + 0451EDB32755BBBD00031A16 /* SearchCalculateUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCalculateUseCase.swift; sourceTree = ""; }; 0476ABBA27310ED200F72DD1 /* BusRouteHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusRouteHeaderView.swift; sourceTree = ""; }; 0476ABBC27311C1600F72DD1 /* BusStationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusStationTableViewCell.swift; sourceTree = ""; }; 047C30F9273A04B40075EA14 /* BBusImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BBusImage.swift; sourceTree = ""; }; @@ -165,6 +407,8 @@ 04AC7D1A2733E7270095CD4E /* AlarmSettingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingButton.swift; sourceTree = ""; }; 04C6D6682734B18A00D41678 /* MovingStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusTableViewCell.swift; sourceTree = ""; }; 04C6D66A2734BCAB00D41678 /* MovingStatusBusTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusBusTagView.swift; sourceTree = ""; }; + 04DC47FA27552FE5003380D9 /* StationCalculateUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationCalculateUseCase.swift; sourceTree = ""; }; + 04DEBDF12757311F00B53D5F /* MOCKArrInfo.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = MOCKArrInfo.json; sourceTree = ""; }; 4A04682327327876008D87CE /* BusRouteCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusRouteCoordinator.swift; sourceTree = ""; }; 4A04682527327BA0008D87CE /* AlarmSettingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingCoordinator.swift; sourceTree = ""; }; 4A0468292732B7B3008D87CE /* SearchNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchNavigationView.swift; sourceTree = ""; }; @@ -180,9 +424,30 @@ 4A1A22DA27326FD100476861 /* HomeNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeNavigationView.swift; sourceTree = ""; }; 4A1A22DC2732801700476861 /* StationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationCoordinator.swift; sourceTree = ""; }; 4A1A22E02732CE7900476861 /* SearchResultCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultCollectionViewCell.swift; sourceTree = ""; }; + 4A2634C027567E5400267B47 /* DummyJsonStringStationByUidItemDTO.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DummyJsonStringStationByUidItemDTO.json; sourceTree = ""; }; 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BBusAPIError.swift; sourceTree = ""; }; 4A38E20827464015003A9D10 /* PublisherExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublisherExtension.swift; sourceTree = ""; }; + 4A3B6C4F2755DF2400BBC498 /* AlarmCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCenter.swift; sourceTree = ""; }; 4A412027274E34430083D691 /* BusRouteModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusRouteModel.swift; sourceTree = ""; }; + 4A5100322754CDB100754B36 /* BaseUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUseCase.swift; sourceTree = ""; }; + 4A5100342754CE1500754B36 /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; + 4A5100362755106A00754B36 /* HomeCalculateUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCalculateUseCase.swift; sourceTree = ""; }; + 4A510038275517F800754B36 /* RefreshableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableView.swift; sourceTree = ""; }; + 4A51003A2755180400754B36 /* RefreshButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshButton.swift; sourceTree = ""; }; + 4A5182A32754E3CA001EA530 /* TokenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenManager.swift; sourceTree = ""; }; + 4A5182A527550988001EA530 /* RequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestFactory.swift; sourceTree = ""; }; + 4A5182A7275511D5001EA530 /* ServiceFetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceFetchable.swift; sourceTree = ""; }; + 4A5182AB275516A4001EA530 /* PersistencetFetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistencetFetchable.swift; sourceTree = ""; }; + 4A5182AE27551E3C001EA530 /* GetArrInfoByRouteListUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetArrInfoByRouteListUsable.swift; sourceTree = ""; }; + 4A5182B027551E84001EA530 /* GetStationsByRouteListUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStationsByRouteListUsable.swift; sourceTree = ""; }; + 4A5182B227551EA9001EA530 /* GetBusPosByRtidUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBusPosByRtidUsable.swift; sourceTree = ""; }; + 4A5182B427551EC2001EA530 /* GetStationByUidItemUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStationByUidItemUsable.swift; sourceTree = ""; }; + 4A5182B627551ED8001EA530 /* GetRouteListUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetRouteListUsable.swift; sourceTree = ""; }; + 4A5182B827551F06001EA530 /* GetBusPosByVehIdUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBusPosByVehIdUsable.swift; sourceTree = ""; }; + 4A5182BA27551F28001EA530 /* GetStationListUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStationListUsable.swift; sourceTree = ""; }; + 4A5182BC27551F40001EA530 /* GetFavoriteItemListUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFavoriteItemListUsable.swift; sourceTree = ""; }; + 4A5182BE27551F57001EA530 /* CreateFavoriteItemUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFavoriteItemUsable.swift; sourceTree = ""; }; + 4A5182C027551F6D001EA530 /* DeleteFavoriteItemUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFavoriteItemUsable.swift; sourceTree = ""; }; 4A7BBFEC2737885F0029915F /* StationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationHeaderView.swift; sourceTree = ""; }; 4A7BBFEE27378AA50029915F /* StationBodyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationBodyCollectionViewCell.swift; sourceTree = ""; }; 4A7BBFF02737D6C20029915F /* RemainCongestionBadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemainCongestionBadgeLabel.swift; sourceTree = ""; }; @@ -199,26 +464,26 @@ 4AA294B6273C1275008E5497 /* DeleteFavoriteItemFetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFavoriteItemFetchable.swift; sourceTree = ""; }; 4AC79160274F6FDB00019827 /* SourceFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFooterView.swift; sourceTree = ""; }; 4ACA51E2272FCD9600EC0531 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = ""; }; - 4ACA51E4272FCD9C00EC0531 /* HomeUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeUseCase.swift; sourceTree = ""; }; + 4ACA51E4272FCD9C00EC0531 /* HomeAPIUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAPIUseCase.swift; sourceTree = ""; }; 4ACA51E6272FCDA600EC0531 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 4ACA51E8272FCDAE00EC0531 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 4ACA51EE272FCDE600EC0531 /* StationSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationSearchResult.swift; sourceTree = ""; }; - 4ACA51F0272FCDF200EC0531 /* SearchUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchUseCase.swift; sourceTree = ""; }; + 4ACA51F0272FCDF200EC0531 /* SearchAPIUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAPIUseCase.swift; sourceTree = ""; }; 4ACA51F2272FCDF900EC0531 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 4ACA51F4272FCE0200EC0531 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; - 4ACA5208272FCE5A00EC0531 /* BusRouteUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusRouteUseCase.swift; sourceTree = ""; }; + 4ACA5208272FCE5A00EC0531 /* BusRouteAPIUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusRouteAPIUseCase.swift; sourceTree = ""; }; 4ACA520A272FCE5F00EC0531 /* BusRouteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusRouteViewModel.swift; sourceTree = ""; }; 4ACA520C272FCE6500EC0531 /* BusRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusRouteView.swift; sourceTree = ""; }; 4ACA5212272FCE8500EC0531 /* BusCongestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusCongestion.swift; sourceTree = ""; }; - 4ACA5214272FCE8A00EC0531 /* StationUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationUseCase.swift; sourceTree = ""; }; + 4ACA5214272FCE8A00EC0531 /* StationAPIUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationAPIUseCase.swift; sourceTree = ""; }; 4ACA5216272FCE8E00EC0531 /* StationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationViewModel.swift; sourceTree = ""; }; 4ACA5218272FCE9500EC0531 /* StationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationView.swift; sourceTree = ""; }; 4ACA521E272FCEAA00EC0531 /* AlarmSettingBusArriveInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingBusArriveInfo.swift; sourceTree = ""; }; - 4ACA5220272FCEAF00EC0531 /* AlarmSettingUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingUseCase.swift; sourceTree = ""; }; + 4ACA5220272FCEAF00EC0531 /* AlarmSettingAPIUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingAPIUseCase.swift; sourceTree = ""; }; 4ACA5222272FCEB500EC0531 /* AlarmSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingViewModel.swift; sourceTree = ""; }; 4ACA5224272FCEBF00EC0531 /* AlarmSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingView.swift; sourceTree = ""; }; 4ACA522A272FCED600EC0531 /* MovingStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusModel.swift; sourceTree = ""; }; - 4ACA522C272FCEDB00EC0531 /* MovingStatusUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusUseCase.swift; sourceTree = ""; }; + 4ACA522C272FCEDB00EC0531 /* MovingStatusAPIUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusAPIUseCase.swift; sourceTree = ""; }; 4ACA522E272FCEDF00EC0531 /* MovingStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusViewModel.swift; sourceTree = ""; }; 4ACA5230272FCEE600EC0531 /* MovingStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusView.swift; sourceTree = ""; }; 4ACA5232272FCF0F00EC0531 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; @@ -234,23 +499,53 @@ 4ADB29BA274B6C8300554A4E /* BusPosByVehicleIdDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusPosByVehicleIdDTO.swift; sourceTree = ""; }; 4ADB29BC274B6E0B00554A4E /* GetBusPosByVehIdFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBusPosByVehIdFetcher.swift; sourceTree = ""; }; 4ADB29C6274B7A7F00554A4E /* GetOnAlarmStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOnAlarmStatus.swift; sourceTree = ""; }; - 4ADB29C8274B7AA200554A4E /* GetOnAlarmUsecase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOnAlarmUsecase.swift; sourceTree = ""; }; + 4ADB29C8274B7AA200554A4E /* GetOnAlarmAPIUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOnAlarmAPIUseCase.swift; sourceTree = ""; }; 4ADB29CA274B7AB900554A4E /* GetOnAlarmViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOnAlarmViewModel.swift; sourceTree = ""; }; - 4ADB29CC274B880C00554A4E /* BusApproachCheckUsecase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusApproachCheckUsecase.swift; sourceTree = ""; }; + 4ADB29CC274B880C00554A4E /* GetOnAlarmCalculateUsecase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOnAlarmCalculateUsecase.swift; sourceTree = ""; }; 4ADB29CE274B890800554A4E /* BusApproachStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusApproachStatus.swift; sourceTree = ""; }; 4ADB29D0274CAF0100554A4E /* AlarmStartResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmStartResult.swift; sourceTree = ""; }; 4ADB29D3274E18BB00554A4E /* GetOffAlarmController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOffAlarmController.swift; sourceTree = ""; }; 4ADB29D6274E198900554A4E /* GetOffAlarmViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOffAlarmViewModel.swift; sourceTree = ""; }; 4ADB29D9274E1A5F00554A4E /* GetOffAlarmStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOffAlarmStatus.swift; sourceTree = ""; }; - 87038A81273B90A50078EAE3 /* BBusAPIUsecases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BBusAPIUsecases.swift; sourceTree = ""; }; - 87038A84273B90D10078EAE3 /* RequestUsecases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestUsecases.swift; sourceTree = ""; }; + 4AF1E05F2755BE2400DE51C8 /* NavigatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatableView.swift; sourceTree = ""; }; + 4AF1E0652756262300DE51C8 /* NetworkServiceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetworkServiceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E0672756262300DE51C8 /* NetworkServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkServiceTests.swift; sourceTree = ""; }; + 4AF1E0722756263400DE51C8 /* PersistenceStorageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PersistenceStorageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E0742756263400DE51C8 /* PersistenceStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceStorageTests.swift; sourceTree = ""; }; + 4AF1E07F2756263E00DE51C8 /* TokenManagerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TokenManagerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E0812756263E00DE51C8 /* TokenManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenManagerTests.swift; sourceTree = ""; }; + 4AF1E08C2756264600DE51C8 /* RequestFactoryTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RequestFactoryTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E08E2756264600DE51C8 /* RequestFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestFactoryTests.swift; sourceTree = ""; }; + 4AF1E0992756266B00DE51C8 /* HomeViewModelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HomeViewModelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E09B2756266B00DE51C8 /* HomeViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModelTests.swift; sourceTree = ""; }; + 4AF1E0A62756268B00DE51C8 /* SearchViewModelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SearchViewModelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E0A82756268B00DE51C8 /* SearchViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModelTests.swift; sourceTree = ""; }; + 4AF1E0B32756269600DE51C8 /* StationViewModelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StationViewModelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E0B52756269600DE51C8 /* StationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationViewModelTests.swift; sourceTree = ""; }; + 4AF1E0C02756269F00DE51C8 /* BusRouteViewModelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusRouteViewModelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E0C22756269F00DE51C8 /* BusRouteViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusRouteViewModelTests.swift; sourceTree = ""; }; + 4AF1E0CD275626A700DE51C8 /* AlarmSettingViewModelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AlarmSettingViewModelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E0CF275626A700DE51C8 /* AlarmSettingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingViewModelTests.swift; sourceTree = ""; }; + 4AF1E0DA275626B300DE51C8 /* MovingStatusViewModelTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MovingStatusViewModelTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4AF1E0DC275626B300DE51C8 /* MovingStatusViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusViewModelTests.swift; sourceTree = ""; }; + 87038A81273B90A50078EAE3 /* BBusAPIUseCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BBusAPIUseCases.swift; sourceTree = ""; }; 87038A87273B950B0078EAE3 /* GetArrInfoByRouteListFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetArrInfoByRouteListFetcher.swift; sourceTree = ""; }; - 87038A89273B96B60078EAE3 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; - 87038A8B273B96DF0078EAE3 /* Persistent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistent.swift; sourceTree = ""; }; + 87038A89273B96B60078EAE3 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; + 87038A8B273B96DF0078EAE3 /* PersistenceStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceStorage.swift; sourceTree = ""; }; 87038A8D273C10480078EAE3 /* GetRouteInfoItemFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetRouteInfoItemFetcher.swift; sourceTree = ""; }; 87038A8F273C11630078EAE3 /* GetStationsByRouteListFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStationsByRouteListFetcher.swift; sourceTree = ""; }; 87038A91273C12320078EAE3 /* GetBusPosByRtidFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBusPosByRtidFetcher.swift; sourceTree = ""; }; 87038A93273C12E20078EAE3 /* GetStationByUidItemFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStationByUidItemFetcher.swift; sourceTree = ""; }; + 87115F162756758800601770 /* GetArrInfoByRouteListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetArrInfoByRouteListUseCase.swift; sourceTree = ""; }; + 87115F18275675B900601770 /* GetStationsByRouteListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStationsByRouteListUseCase.swift; sourceTree = ""; }; + 87115F1A275675E200601770 /* GetBusPosByRtidUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBusPosByRtidUseCase.swift; sourceTree = ""; }; + 87115F1C2756761200601770 /* GetStationByUidItemUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStationByUidItemUseCase.swift; sourceTree = ""; }; + 87115F1E2756766900601770 /* GetBusPosByVehIdUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBusPosByVehIdUseCase.swift; sourceTree = ""; }; + 87115F202756768900601770 /* GetRouteListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetRouteListUseCase.swift; sourceTree = ""; }; + 87115F222756769D00601770 /* GetStationListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStationListUseCase.swift; sourceTree = ""; }; + 87115F24275676B000601770 /* GetFavoriteItemListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFavoriteItemListUseCase.swift; sourceTree = ""; }; + 87115F26275676C500601770 /* CreateFavoriteItemUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFavoriteItemUseCase.swift; sourceTree = ""; }; + 87115F28275676E900601770 /* DeleteFavoriteItemUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFavoriteItemUseCase.swift; sourceTree = ""; }; 87285F5427461DB300CA3BA9 /* UINavigationControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationControllerExtension.swift; sourceTree = ""; }; 873578822732545D00CC8ECC /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.swift; sourceTree = ""; }; 87359B8C27311BF100F461A7 /* BusTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusTagView.swift; sourceTree = ""; }; @@ -258,6 +553,8 @@ 873D639727303A6800E79069 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 873D639927303B0500E79069 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = ""; }; 873D639B27303B5000E79069 /* SearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCoordinator.swift; sourceTree = ""; }; + 875F1AAF2755BD86003F5BB1 /* AverageSectionTimeCalculatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AverageSectionTimeCalculatable.swift; sourceTree = ""; }; + 875F1AB12755BE08003F5BB1 /* AlarmSettingCalculateUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingCalculateUseCase.swift; sourceTree = ""; }; 87A555682728116400A9B5E3 /* BBus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BBus.app; sourceTree = BUILT_PRODUCTS_DIR; }; 87A5556B2728116400A9B5E3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 87A5556D2728116400A9B5E3 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -265,6 +562,7 @@ 87A555742728116600A9B5E3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 87A555772728116600A9B5E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 87A555792728116600A9B5E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 87B35D092754E71F00159791 /* MovingStatusCalculateUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingStatusCalculateUseCase.swift; sourceTree = ""; }; 87EB52AD2733A6C6000D5492 /* GetOnStatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetOnStatusCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -276,6 +574,76 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4AF1E0622756262300DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E06F2756263400DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E07C2756263E00DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0892756264600DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0962756266B00DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0A32756268B00DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0B02756269600DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0BD2756269F00DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0CA275626A700DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0D7275626B300DE51C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 87A555652728116400A9B5E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -311,13 +679,16 @@ 047C30FF273A06980075EA14 /* Global */ = { isa = PBXGroup; children = ( + 4A3B6C4E2755DF0400BBC498 /* DeviceConfig */, + 875F1AAE2755BD77003F5BB1 /* UseCase */, 049375A9273B9F200061ACDA /* DTO */, 049375A4273B9C970061ACDA /* Resource */, 87038A80273B90810078EAE3 /* Network */, 873D6394273039C400E79069 /* Coordinator */, 047C30F8273A044C0075EA14 /* Constant */, 041A5A0F273D2E0400490075 /* Extension */, - 049B9CFE2744D27F004DE66E /* View */, + 049B9CFE2744D27F004DE66E /* CustomView */, + 4A5100312754CD5800754B36 /* Base */, ); path = Global; sourceTree = ""; @@ -342,17 +713,55 @@ 049375AA273B9F330061ACDA /* BusRouteDTO.swift */, 049375AE273BAA130061ACDA /* StationDTO.swift */, 4A06AEC7274159D10027222D /* FavoriteItemDTO.swift */, + 0446843C275679B9007E440A /* JsonDTO.swift */, ); path = DTO; sourceTree = ""; }; - 049B9CFE2744D27F004DE66E /* View */ = { + 049B9CFE2744D27F004DE66E /* CustomView */ = { isa = PBXGroup; children = ( 049B9CFF2744D2AA004DE66E /* ThrottleButton.swift */, 873578822732545D00CC8ECC /* CustomNavigationBar.swift */, + 4A510038275517F800754B36 /* RefreshableView.swift */, + 4A51003A2755180400754B36 /* RefreshButton.swift */, + 4AF1E05F2755BE2400DE51C8 /* NavigatableView.swift */, ); - path = View; + path = CustomView; + sourceTree = ""; + }; + 4A3B6C4E2755DF0400BBC498 /* DeviceConfig */ = { + isa = PBXGroup; + children = ( + 4A3B6C4F2755DF2400BBC498 /* AlarmCenter.swift */, + ); + path = DeviceConfig; + sourceTree = ""; + }; + 4A5100312754CD5800754B36 /* Base */ = { + isa = PBXGroup; + children = ( + 4A5100322754CDB100754B36 /* BaseUseCase.swift */, + 4A5100342754CE1500754B36 /* BaseViewController.swift */, + ); + path = Base; + sourceTree = ""; + }; + 4A5182AD27551E0D001EA530 /* RequestUseCases */ = { + isa = PBXGroup; + children = ( + 4A5182AE27551E3C001EA530 /* GetArrInfoByRouteListUsable.swift */, + 4A5182B027551E84001EA530 /* GetStationsByRouteListUsable.swift */, + 4A5182B227551EA9001EA530 /* GetBusPosByRtidUsable.swift */, + 4A5182B427551EC2001EA530 /* GetStationByUidItemUsable.swift */, + 4A5182B827551F06001EA530 /* GetBusPosByVehIdUsable.swift */, + 4A5182B627551ED8001EA530 /* GetRouteListUsable.swift */, + 4A5182BA27551F28001EA530 /* GetStationListUsable.swift */, + 4A5182BC27551F40001EA530 /* GetFavoriteItemListUsable.swift */, + 4A5182BE27551F57001EA530 /* CreateFavoriteItemUsable.swift */, + 4A5182C027551F6D001EA530 /* DeleteFavoriteItemUsable.swift */, + ); + path = RequestUseCases; sourceTree = ""; }; 4ACA51D7272FCC4E00EC0531 /* Home */ = { @@ -459,7 +868,10 @@ 4ACA51E0272FCD7100EC0531 /* UseCase */ = { isa = PBXGroup; children = ( - 4ACA51E4272FCD9C00EC0531 /* HomeUseCase.swift */, + 0446842A27567654007E440A /* HomeAPIUsable.swift */, + 0446842C27567660007E440A /* HomeCalculatable.swift */, + 4ACA51E4272FCD9C00EC0531 /* HomeAPIUseCase.swift */, + 4A5100362755106A00754B36 /* HomeCalculateUseCase.swift */, ); path = UseCase; sourceTree = ""; @@ -497,7 +909,10 @@ 4ACA51EC272FCDD000EC0531 /* UseCase */ = { isa = PBXGroup; children = ( - 4ACA51F0272FCDF200EC0531 /* SearchUseCase.swift */, + 04468426275675F1007E440A /* SearchAPIUsable.swift */, + 044684282756760A007E440A /* SearchCalculatable.swift */, + 4ACA51F0272FCDF200EC0531 /* SearchAPIUseCase.swift */, + 0451EDB32755BBBD00031A16 /* SearchCalculateUseCase.swift */, ); path = UseCase; sourceTree = ""; @@ -535,7 +950,8 @@ 4ACA5204272FCE3900EC0531 /* UseCase */ = { isa = PBXGroup; children = ( - 4ACA5208272FCE5A00EC0531 /* BusRouteUseCase.swift */, + 0446842E275676B3007E440A /* BusRouteAPIUsable.swift */, + 4ACA5208272FCE5A00EC0531 /* BusRouteAPIUseCase.swift */, ); path = UseCase; sourceTree = ""; @@ -569,7 +985,10 @@ 4ACA5210272FCE6E00EC0531 /* UseCase */ = { isa = PBXGroup; children = ( - 4ACA5214272FCE8A00EC0531 /* StationUseCase.swift */, + 04468430275676ED007E440A /* StationAPIUsable.swift */, + 04468432275676F8007E440A /* StationCalculatable.swift */, + 4ACA5214272FCE8A00EC0531 /* StationAPIUseCase.swift */, + 04DC47FA27552FE5003380D9 /* StationCalculateUseCase.swift */, ); path = UseCase; sourceTree = ""; @@ -608,7 +1027,10 @@ 4ACA521C272FCE9E00EC0531 /* UseCase */ = { isa = PBXGroup; children = ( - 4ACA5220272FCEAF00EC0531 /* AlarmSettingUseCase.swift */, + 0446843427567769007E440A /* AlarmSettingAPIUsable.swift */, + 0446843627567777007E440A /* AlarmSettingCalculatable.swift */, + 4ACA5220272FCEAF00EC0531 /* AlarmSettingAPIUseCase.swift */, + 875F1AB12755BE08003F5BB1 /* AlarmSettingCalculateUseCase.swift */, ); path = UseCase; sourceTree = ""; @@ -643,7 +1065,10 @@ 4ACA5228272FCEC800EC0531 /* UseCase */ = { isa = PBXGroup; children = ( - 4ACA522C272FCEDB00EC0531 /* MovingStatusUseCase.swift */, + 04468438275677C0007E440A /* MovingStatusAPIUsable.swift */, + 0446843A275677CA007E440A /* MovingStatusCalculatable.swift */, + 4ACA522C272FCEDB00EC0531 /* MovingStatusAPIUseCase.swift */, + 87B35D092754E71F00159791 /* MovingStatusCalculateUseCase.swift */, ); path = UseCase; sourceTree = ""; @@ -707,8 +1132,8 @@ 4ADB29C4274B7A3000554A4E /* UseCase */ = { isa = PBXGroup; children = ( - 4ADB29C8274B7AA200554A4E /* GetOnAlarmUsecase.swift */, - 4ADB29CC274B880C00554A4E /* BusApproachCheckUsecase.swift */, + 4ADB29C8274B7AA200554A4E /* GetOnAlarmAPIUseCase.swift */, + 4ADB29CC274B880C00554A4E /* GetOnAlarmCalculateUsecase.swift */, ); path = UseCase; sourceTree = ""; @@ -750,14 +1175,98 @@ path = Model; sourceTree = ""; }; + 4AF1E0662756262300DE51C8 /* NetworkServiceTests */ = { + isa = PBXGroup; + children = ( + 4AF1E0672756262300DE51C8 /* NetworkServiceTests.swift */, + ); + path = NetworkServiceTests; + sourceTree = ""; + }; + 4AF1E0732756263400DE51C8 /* PersistenceStorageTests */ = { + isa = PBXGroup; + children = ( + 4AF1E0742756263400DE51C8 /* PersistenceStorageTests.swift */, + ); + path = PersistenceStorageTests; + sourceTree = ""; + }; + 4AF1E0802756263E00DE51C8 /* TokenManagerTests */ = { + isa = PBXGroup; + children = ( + 4AF1E0812756263E00DE51C8 /* TokenManagerTests.swift */, + ); + path = TokenManagerTests; + sourceTree = ""; + }; + 4AF1E08D2756264600DE51C8 /* RequestFactoryTests */ = { + isa = PBXGroup; + children = ( + 4AF1E08E2756264600DE51C8 /* RequestFactoryTests.swift */, + ); + path = RequestFactoryTests; + sourceTree = ""; + }; + 4AF1E09A2756266B00DE51C8 /* HomeViewModelTests */ = { + isa = PBXGroup; + children = ( + 4AF1E09B2756266B00DE51C8 /* HomeViewModelTests.swift */, + ); + path = HomeViewModelTests; + sourceTree = ""; + }; + 4AF1E0A72756268B00DE51C8 /* SearchViewModelTests */ = { + isa = PBXGroup; + children = ( + 4AF1E0A82756268B00DE51C8 /* SearchViewModelTests.swift */, + ); + path = SearchViewModelTests; + sourceTree = ""; + }; + 4AF1E0B42756269600DE51C8 /* StationViewModelTests */ = { + isa = PBXGroup; + children = ( + 4AF1E0B52756269600DE51C8 /* StationViewModelTests.swift */, + 4A2634C027567E5400267B47 /* DummyJsonStringStationByUidItemDTO.json */, + ); + path = StationViewModelTests; + sourceTree = ""; + }; + 4AF1E0C12756269F00DE51C8 /* BusRouteViewModelTests */ = { + isa = PBXGroup; + children = ( + 4AF1E0C22756269F00DE51C8 /* BusRouteViewModelTests.swift */, + ); + path = BusRouteViewModelTests; + sourceTree = ""; + }; + 4AF1E0CE275626A700DE51C8 /* AlarmSettingViewModelTests */ = { + isa = PBXGroup; + children = ( + 4AF1E0CF275626A700DE51C8 /* AlarmSettingViewModelTests.swift */, + 04DEBDF12757311F00B53D5F /* MOCKArrInfo.json */, + ); + path = AlarmSettingViewModelTests; + sourceTree = ""; + }; + 4AF1E0DB275626B300DE51C8 /* MovingStatusViewModelTests */ = { + isa = PBXGroup; + children = ( + 4AF1E0DC275626B300DE51C8 /* MovingStatusViewModelTests.swift */, + ); + path = MovingStatusViewModelTests; + sourceTree = ""; + }; 87038A80273B90810078EAE3 /* Network */ = { isa = PBXGroup; children = ( + 87038A89273B96B60078EAE3 /* NetworkService.swift */, + 87038A8B273B96DF0078EAE3 /* PersistenceStorage.swift */, + 4A5182A32754E3CA001EA530 /* TokenManager.swift */, + 4A5182A527550988001EA530 /* RequestFactory.swift */, 4A38E20627463FF7003A9D10 /* BBusAPIError.swift */, - 87038A81273B90A50078EAE3 /* BBusAPIUsecases.swift */, - 87038A84273B90D10078EAE3 /* RequestUsecases.swift */, - 87038A89273B96B60078EAE3 /* Service.swift */, - 87038A8B273B96DF0078EAE3 /* Persistent.swift */, + 87115F152756747400601770 /* BBusAPIUseCases */, + 4A5182AD27551E0D001EA530 /* RequestUseCases */, 87038A86273B94CD0078EAE3 /* Fetcher */, 4A06AEC2273E51AE0027222D /* AccessKey.xcconfig */, ); @@ -767,6 +1276,8 @@ 87038A86273B94CD0078EAE3 /* Fetcher */ = { isa = PBXGroup; children = ( + 4A5182A7275511D5001EA530 /* ServiceFetchable.swift */, + 4A5182AB275516A4001EA530 /* PersistencetFetchable.swift */, 87038A87273B950B0078EAE3 /* GetArrInfoByRouteListFetcher.swift */, 4AA294AE273C0E8D008E5497 /* GetRouteListFetcher.swift */, 4AA294B0273C1094008E5497 /* GetStationListFetcher.swift */, @@ -782,6 +1293,24 @@ path = Fetcher; sourceTree = ""; }; + 87115F152756747400601770 /* BBusAPIUseCases */ = { + isa = PBXGroup; + children = ( + 87038A81273B90A50078EAE3 /* BBusAPIUseCases.swift */, + 87115F162756758800601770 /* GetArrInfoByRouteListUseCase.swift */, + 87115F18275675B900601770 /* GetStationsByRouteListUseCase.swift */, + 87115F1A275675E200601770 /* GetBusPosByRtidUseCase.swift */, + 87115F1C2756761200601770 /* GetStationByUidItemUseCase.swift */, + 87115F1E2756766900601770 /* GetBusPosByVehIdUseCase.swift */, + 87115F202756768900601770 /* GetRouteListUseCase.swift */, + 87115F222756769D00601770 /* GetStationListUseCase.swift */, + 87115F24275676B000601770 /* GetFavoriteItemListUseCase.swift */, + 87115F26275676C500601770 /* CreateFavoriteItemUseCase.swift */, + 87115F28275676E900601770 /* DeleteFavoriteItemUseCase.swift */, + ); + path = BBusAPIUseCases; + sourceTree = ""; + }; 873D6394273039C400E79069 /* Coordinator */ = { isa = PBXGroup; children = ( @@ -792,11 +1321,29 @@ path = Coordinator; sourceTree = ""; }; + 875F1AAE2755BD77003F5BB1 /* UseCase */ = { + isa = PBXGroup; + children = ( + 875F1AAF2755BD86003F5BB1 /* AverageSectionTimeCalculatable.swift */, + ); + path = UseCase; + sourceTree = ""; + }; 87A5555F2728116400A9B5E3 = { isa = PBXGroup; children = ( 87A5556A2728116400A9B5E3 /* BBus */, 4ACA5243272FD52D00EC0531 /* BBusTests */, + 4AF1E0662756262300DE51C8 /* NetworkServiceTests */, + 4AF1E0732756263400DE51C8 /* PersistenceStorageTests */, + 4AF1E0802756263E00DE51C8 /* TokenManagerTests */, + 4AF1E08D2756264600DE51C8 /* RequestFactoryTests */, + 4AF1E09A2756266B00DE51C8 /* HomeViewModelTests */, + 4AF1E0A72756268B00DE51C8 /* SearchViewModelTests */, + 4AF1E0B42756269600DE51C8 /* StationViewModelTests */, + 4AF1E0C12756269F00DE51C8 /* BusRouteViewModelTests */, + 4AF1E0CE275626A700DE51C8 /* AlarmSettingViewModelTests */, + 4AF1E0DB275626B300DE51C8 /* MovingStatusViewModelTests */, 87A555692728116400A9B5E3 /* Products */, ); sourceTree = ""; @@ -806,6 +1353,16 @@ children = ( 87A555682728116400A9B5E3 /* BBus.app */, 4ACA5242272FD52D00EC0531 /* BBusTests.xctest */, + 4AF1E0652756262300DE51C8 /* NetworkServiceTests.xctest */, + 4AF1E0722756263400DE51C8 /* PersistenceStorageTests.xctest */, + 4AF1E07F2756263E00DE51C8 /* TokenManagerTests.xctest */, + 4AF1E08C2756264600DE51C8 /* RequestFactoryTests.xctest */, + 4AF1E0992756266B00DE51C8 /* HomeViewModelTests.xctest */, + 4AF1E0A62756268B00DE51C8 /* SearchViewModelTests.xctest */, + 4AF1E0B32756269600DE51C8 /* StationViewModelTests.xctest */, + 4AF1E0C02756269F00DE51C8 /* BusRouteViewModelTests.xctest */, + 4AF1E0CD275626A700DE51C8 /* AlarmSettingViewModelTests.xctest */, + 4AF1E0DA275626B300DE51C8 /* MovingStatusViewModelTests.xctest */, ); name = Products; sourceTree = ""; @@ -846,73 +1403,375 @@ productReference = 4ACA5242272FD52D00EC0531 /* BBusTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 87A555672728116400A9B5E3 /* BBus */ = { + 4AF1E0642756262300DE51C8 /* NetworkServiceTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 87A5557C2728116600A9B5E3 /* Build configuration list for PBXNativeTarget "BBus" */; + buildConfigurationList = 4AF1E06D2756262300DE51C8 /* Build configuration list for PBXNativeTarget "NetworkServiceTests" */; buildPhases = ( - 87A555642728116400A9B5E3 /* Sources */, - 87A555652728116400A9B5E3 /* Frameworks */, - 87A555662728116400A9B5E3 /* Resources */, + 4AF1E0612756262300DE51C8 /* Sources */, + 4AF1E0622756262300DE51C8 /* Frameworks */, + 4AF1E0632756262300DE51C8 /* Resources */, ); buildRules = ( ); dependencies = ( + 4AF1E06A2756262300DE51C8 /* PBXTargetDependency */, ); - name = BBus; - productName = BBus; - productReference = 87A555682728116400A9B5E3 /* BBus.app */; - productType = "com.apple.product-type.application"; + name = NetworkServiceTests; + productName = NetworkServiceTests; + productReference = 4AF1E0652756262300DE51C8 /* NetworkServiceTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 87A555602728116400A9B5E3 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1310; - LastUpgradeCheck = 1300; - TargetAttributes = { - 4ACA5241272FD52D00EC0531 = { - CreatedOnToolsVersion = 13.1; - TestTargetID = 87A555672728116400A9B5E3; - }; - 87A555672728116400A9B5E3 = { - CreatedOnToolsVersion = 13.0; - }; - }; - }; - buildConfigurationList = 87A555632728116400A9B5E3 /* Build configuration list for PBXProject "BBus" */; - compatibilityVersion = "Xcode 13.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, + 4AF1E0712756263400DE51C8 /* PersistenceStorageTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AF1E0782756263400DE51C8 /* Build configuration list for PBXNativeTarget "PersistenceStorageTests" */; + buildPhases = ( + 4AF1E06E2756263400DE51C8 /* Sources */, + 4AF1E06F2756263400DE51C8 /* Frameworks */, + 4AF1E0702756263400DE51C8 /* Resources */, ); - mainGroup = 87A5555F2728116400A9B5E3; - productRefGroup = 87A555692728116400A9B5E3 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 87A555672728116400A9B5E3 /* BBus */, - 4ACA5241272FD52D00EC0531 /* BBusTests */, + buildRules = ( ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 4ACA5240272FD52D00EC0531 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + dependencies = ( + 4AF1E0772756263400DE51C8 /* PBXTargetDependency */, ); - runOnlyForDeploymentPostprocessing = 0; + name = PersistenceStorageTests; + productName = PersistenceStorageTests; + productReference = 4AF1E0722756263400DE51C8 /* PersistenceStorageTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; }; - 87A555662728116400A9B5E3 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( + 4AF1E07E2756263E00DE51C8 /* TokenManagerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AF1E0852756263E00DE51C8 /* Build configuration list for PBXNativeTarget "TokenManagerTests" */; + buildPhases = ( + 4AF1E07B2756263E00DE51C8 /* Sources */, + 4AF1E07C2756263E00DE51C8 /* Frameworks */, + 4AF1E07D2756263E00DE51C8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4AF1E0842756263E00DE51C8 /* PBXTargetDependency */, + ); + name = TokenManagerTests; + productName = TokenManagerTests; + productReference = 4AF1E07F2756263E00DE51C8 /* TokenManagerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4AF1E08B2756264600DE51C8 /* RequestFactoryTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AF1E0922756264600DE51C8 /* Build configuration list for PBXNativeTarget "RequestFactoryTests" */; + buildPhases = ( + 4AF1E0882756264600DE51C8 /* Sources */, + 4AF1E0892756264600DE51C8 /* Frameworks */, + 4AF1E08A2756264600DE51C8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4AF1E0912756264600DE51C8 /* PBXTargetDependency */, + ); + name = RequestFactoryTests; + productName = RequestFactoryTests; + productReference = 4AF1E08C2756264600DE51C8 /* RequestFactoryTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4AF1E0982756266B00DE51C8 /* HomeViewModelTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AF1E09F2756266B00DE51C8 /* Build configuration list for PBXNativeTarget "HomeViewModelTests" */; + buildPhases = ( + 4AF1E0952756266B00DE51C8 /* Sources */, + 4AF1E0962756266B00DE51C8 /* Frameworks */, + 4AF1E0972756266B00DE51C8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4AF1E09E2756266B00DE51C8 /* PBXTargetDependency */, + ); + name = HomeViewModelTests; + productName = HomeViewModelTests; + productReference = 4AF1E0992756266B00DE51C8 /* HomeViewModelTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4AF1E0A52756268B00DE51C8 /* SearchViewModelTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AF1E0AC2756268B00DE51C8 /* Build configuration list for PBXNativeTarget "SearchViewModelTests" */; + buildPhases = ( + 4AF1E0A22756268B00DE51C8 /* Sources */, + 4AF1E0A32756268B00DE51C8 /* Frameworks */, + 4AF1E0A42756268B00DE51C8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4AF1E0AB2756268B00DE51C8 /* PBXTargetDependency */, + ); + name = SearchViewModelTests; + productName = SearchViewModelTests; + productReference = 4AF1E0A62756268B00DE51C8 /* SearchViewModelTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4AF1E0B22756269600DE51C8 /* StationViewModelTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AF1E0B92756269600DE51C8 /* Build configuration list for PBXNativeTarget "StationViewModelTests" */; + buildPhases = ( + 4AF1E0AF2756269600DE51C8 /* Sources */, + 4AF1E0B02756269600DE51C8 /* Frameworks */, + 4AF1E0B12756269600DE51C8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4AF1E0B82756269600DE51C8 /* PBXTargetDependency */, + ); + name = StationViewModelTests; + productName = StationViewModelTests; + productReference = 4AF1E0B32756269600DE51C8 /* StationViewModelTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4AF1E0BF2756269F00DE51C8 /* BusRouteViewModelTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AF1E0C62756269F00DE51C8 /* Build configuration list for PBXNativeTarget "BusRouteViewModelTests" */; + buildPhases = ( + 4AF1E0BC2756269F00DE51C8 /* Sources */, + 4AF1E0BD2756269F00DE51C8 /* Frameworks */, + 4AF1E0BE2756269F00DE51C8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4AF1E0C52756269F00DE51C8 /* PBXTargetDependency */, + ); + name = BusRouteViewModelTests; + productName = BusRouteViewModelTests; + productReference = 4AF1E0C02756269F00DE51C8 /* BusRouteViewModelTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4AF1E0CC275626A700DE51C8 /* AlarmSettingViewModelTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AF1E0D3275626A700DE51C8 /* Build configuration list for PBXNativeTarget "AlarmSettingViewModelTests" */; + buildPhases = ( + 4AF1E0C9275626A700DE51C8 /* Sources */, + 4AF1E0CA275626A700DE51C8 /* Frameworks */, + 4AF1E0CB275626A700DE51C8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4AF1E0D2275626A700DE51C8 /* PBXTargetDependency */, + ); + name = AlarmSettingViewModelTests; + productName = AlarmSettingViewModelTests; + productReference = 4AF1E0CD275626A700DE51C8 /* AlarmSettingViewModelTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4AF1E0D9275626B300DE51C8 /* MovingStatusViewModelTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4AF1E0E0275626B300DE51C8 /* Build configuration list for PBXNativeTarget "MovingStatusViewModelTests" */; + buildPhases = ( + 4AF1E0D6275626B300DE51C8 /* Sources */, + 4AF1E0D7275626B300DE51C8 /* Frameworks */, + 4AF1E0D8275626B300DE51C8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4AF1E0DF275626B300DE51C8 /* PBXTargetDependency */, + ); + name = MovingStatusViewModelTests; + productName = MovingStatusViewModelTests; + productReference = 4AF1E0DA275626B300DE51C8 /* MovingStatusViewModelTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 87A555672728116400A9B5E3 /* BBus */ = { + isa = PBXNativeTarget; + buildConfigurationList = 87A5557C2728116600A9B5E3 /* Build configuration list for PBXNativeTarget "BBus" */; + buildPhases = ( + 87A555642728116400A9B5E3 /* Sources */, + 87A555652728116400A9B5E3 /* Frameworks */, + 87A555662728116400A9B5E3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = BBus; + productName = BBus; + productReference = 87A555682728116400A9B5E3 /* BBus.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 87A555602728116400A9B5E3 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1310; + LastUpgradeCheck = 1300; + TargetAttributes = { + 4ACA5241272FD52D00EC0531 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E0642756262300DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E0712756263400DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E07E2756263E00DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E08B2756264600DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E0982756266B00DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E0A52756268B00DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E0B22756269600DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E0BF2756269F00DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E0CC275626A700DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 4AF1E0D9275626B300DE51C8 = { + CreatedOnToolsVersion = 13.1; + TestTargetID = 87A555672728116400A9B5E3; + }; + 87A555672728116400A9B5E3 = { + CreatedOnToolsVersion = 13.0; + }; + }; + }; + buildConfigurationList = 87A555632728116400A9B5E3 /* Build configuration list for PBXProject "BBus" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 87A5555F2728116400A9B5E3; + productRefGroup = 87A555692728116400A9B5E3 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 87A555672728116400A9B5E3 /* BBus */, + 4ACA5241272FD52D00EC0531 /* BBusTests */, + 4AF1E0642756262300DE51C8 /* NetworkServiceTests */, + 4AF1E0712756263400DE51C8 /* PersistenceStorageTests */, + 4AF1E07E2756263E00DE51C8 /* TokenManagerTests */, + 4AF1E08B2756264600DE51C8 /* RequestFactoryTests */, + 4AF1E0982756266B00DE51C8 /* HomeViewModelTests */, + 4AF1E0A52756268B00DE51C8 /* SearchViewModelTests */, + 4AF1E0B22756269600DE51C8 /* StationViewModelTests */, + 4AF1E0BF2756269F00DE51C8 /* BusRouteViewModelTests */, + 4AF1E0CC275626A700DE51C8 /* AlarmSettingViewModelTests */, + 4AF1E0D9275626B300DE51C8 /* MovingStatusViewModelTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4ACA5240272FD52D00EC0531 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0632756262300DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0702756263400DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E07D2756263E00DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E08A2756264600DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0972756266B00DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0A42756268B00DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0B12756269600DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4A2634C5275693BC00267B47 /* DummyJsonStringStationByUidItemDTO.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0BE2756269F00DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0CB275626A700DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 04DEBDF22757311F00B53D5F /* MOCKArrInfo.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0D8275626B300DE51C8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 87A555662728116400A9B5E3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( 87A555782728116600A9B5E3 /* LaunchScreen.storyboard in Resources */, 049375A6273B9CB60061ACDA /* BusRouteList.json in Resources */, 049375A8273B9CD60061ACDA /* StationList.json in Resources */, @@ -931,65 +1790,271 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 87A555642728116400A9B5E3 /* Sources */ = { + 4AF1E0612756262300DE51C8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4ACA5231272FCEE600EC0531 /* MovingStatusView.swift in Sources */, - 4ADB29D7274E198900554A4E /* GetOffAlarmViewModel.swift in Sources */, - 4ACA51EF272FCDE600EC0531 /* StationSearchResult.swift in Sources */, - 4A17CED92733B5C6007B1646 /* SearchResultScrollView.swift in Sources */, - 4ACA523B272FCF3C00EC0531 /* AlarmSettingViewController.swift in Sources */, - 4ACA5221272FCEAF00EC0531 /* AlarmSettingUseCase.swift in Sources */, - 4ACA51F1272FCDF200EC0531 /* SearchUseCase.swift in Sources */, - 4A094CDF27435C5900428F55 /* BusRemainTime.swift in Sources */, - 4AA294B5273C11DE008E5497 /* CreateFavoriteItemFetcher.swift in Sources */, - 4ACA5239272FCF3200EC0531 /* StationViewController.swift in Sources */, - 873D6396273039E100E79069 /* Coordinator.swift in Sources */, - 4ADB29C9274B7AA200554A4E /* GetOnAlarmUsecase.swift in Sources */, - 0476ABBD27311C1600F72DD1 /* BusStationTableViewCell.swift in Sources */, - 4A38E20727463FF7003A9D10 /* BBusAPIError.swift in Sources */, - 87038A8C273B96DF0078EAE3 /* Persistent.swift in Sources */, - 4ADB29CF274B890800554A4E /* BusApproachStatus.swift in Sources */, - 0476ABBB27310ED200F72DD1 /* BusRouteHeaderView.swift in Sources */, - 4AA294B7273C1275008E5497 /* DeleteFavoriteItemFetchable.swift in Sources */, - 4ACA51F5272FCE0200EC0531 /* SearchView.swift in Sources */, - 04C6D66B2734BCAB00D41678 /* MovingStatusBusTagView.swift in Sources */, - 049375B9273BE2640061ACDA /* StationByUidItemDTO.swift in Sources */, - 4ADB29CB274B7AB900554A4E /* GetOnAlarmViewModel.swift in Sources */, - 4ADB29CD274B880C00554A4E /* BusApproachCheckUsecase.swift in Sources */, - 4ACA520D272FCE6500EC0531 /* BusRouteView.swift in Sources */, - 4ADB29B9274B68CE00554A4E /* GetOnAlarmController.swift in Sources */, - 87038A88273B950B0078EAE3 /* GetArrInfoByRouteListFetcher.swift in Sources */, - 87038A82273B90A50078EAE3 /* BBusAPIUsecases.swift in Sources */, - 4ACA51E3272FCD9600EC0531 /* HomeModel.swift in Sources */, - 4ACA51E9272FCDAE00EC0531 /* HomeView.swift in Sources */, - 4ADB29D4274E18BB00554A4E /* GetOffAlarmController.swift in Sources */, - 4A06AEC8274159D10027222D /* FavoriteItemDTO.swift in Sources */, - 4ACA521F272FCEAA00EC0531 /* AlarmSettingBusArriveInfo.swift in Sources */, + 4A2634AB2756678100267B47 /* BBusAPIError.swift in Sources */, + 4A2634AA2756674900267B47 /* NetworkService.swift in Sources */, + 4AF1E0682756262300DE51C8 /* NetworkServiceTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E06E2756263400DE51C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4AF1E0E827567DE100DE51C8 /* JsonDTO.swift in Sources */, + 4AF1E0E72756735B00DE51C8 /* FavoriteItemDTO.swift in Sources */, + 4AF1E0E62756734A00DE51C8 /* BBusAPIError.swift in Sources */, + 4AF1E0E42756725900DE51C8 /* PublisherExtension.swift in Sources */, + 4AF1E0E327566C8600DE51C8 /* PersistenceStorage.swift in Sources */, + 4AF1E0752756263400DE51C8 /* PersistenceStorageTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E07B2756263E00DE51C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0446840D2756597F007E440A /* BBusAPIError.swift in Sources */, + 4AF1E0822756263E00DE51C8 /* TokenManagerTests.swift in Sources */, + 0446840C275650B0007E440A /* TokenManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0882756264600DE51C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 87115EFA27564D0F00601770 /* RequestFactory.swift in Sources */, + 4AF1E08F2756264600DE51C8 /* RequestFactoryTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0952756266B00DE51C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4AF1E09C2756266B00DE51C8 /* HomeViewModelTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0A22756268B00DE51C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0446844727568119007E440A /* PublisherExtension.swift in Sources */, + 044684482756812D007E440A /* JsonDTO.swift in Sources */, + 0446843E27567F4E007E440A /* SearchViewModel.swift in Sources */, + 4AF1E0A92756268B00DE51C8 /* SearchViewModelTests.swift in Sources */, + 0446844D275694D7007E440A /* SearchCalculateUseCase.swift in Sources */, + 04468445275680B1007E440A /* BaseUseCase.swift in Sources */, + 0446844127568084007E440A /* BusRouteDTO.swift in Sources */, + 0446844927568134007E440A /* BBusAPIError.swift in Sources */, + 0446844027568075007E440A /* SearchCalculatable.swift in Sources */, + 0446844A27568A3B007E440A /* StringExtension.swift in Sources */, + 0446844327568096007E440A /* SearchResults.swift in Sources */, + 0446844227568091007E440A /* StationDTO.swift in Sources */, + 0446843F27568065007E440A /* SearchAPIUsable.swift in Sources */, + 04468444275680A0007E440A /* BusSearchResult.swift in Sources */, + 04468446275680FF007E440A /* StationSearchResult.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0AF2756269600DE51C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4A2634BB2756758C00267B47 /* BusCongestion.swift in Sources */, + 4A2634B0275670BE00267B47 /* StationByUidItemDTO.swift in Sources */, + 4A2634AF275670A500267B47 /* BaseUseCase.swift in Sources */, + 4A2634C22756863600267B47 /* StationCalculatable.swift in Sources */, + 4A2634BC2756759700267B47 /* AlarmSettingBusArriveInfo.swift in Sources */, + 4A2634B1275670C500267B47 /* FavoriteItemDTO.swift in Sources */, + 4A2634B3275670D100267B47 /* StationDTO.swift in Sources */, + 4A2634BA2756757F00267B47 /* PublisherExtension.swift in Sources */, + 4A2634BE275675C400267B47 /* BusRemainTime.swift in Sources */, + 4A2634BF275675CD00267B47 /* BusPosByVehicleIdDTO.swift in Sources */, + 4A2634C12756862F00267B47 /* StationAPIUsable.swift in Sources */, + 4A2634B62756754E00267B47 /* BusSectionKeys.swift in Sources */, + 4A2634BD275675A800267B47 /* BBusAPIError.swift in Sources */, + 4A2634C32756864A00267B47 /* JsonDTO.swift in Sources */, + 4AF1E0B62756269600DE51C8 /* StationViewModelTests.swift in Sources */, + 4A2634B72756755700267B47 /* BBusRouteType.swift in Sources */, + 4A2634B92756757400267B47 /* NotificationNameExtension.swift in Sources */, + 4A2634B82756756200267B47 /* BusArriveInfos.swift in Sources */, + 4A2634C427568CB900267B47 /* StationCalculateUseCase.swift in Sources */, + 4A2634B2275670CB00267B47 /* BusRouteDTO.swift in Sources */, + 4A2634AC27566F8600267B47 /* StationViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0BC2756269F00DE51C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 87115F0F27566A8E00601770 /* GetBusPosByRtidFetcher.swift in Sources */, + 875483792756810D00136F16 /* JsonDTO.swift in Sources */, + 87115F1027566A9800601770 /* GetRouteListFetcher.swift in Sources */, + 87115F0B27566A6900601770 /* RequestFactory.swift in Sources */, + 87115F042756694600601770 /* PublisherExtension.swift in Sources */, + 87115F02275668F600601770 /* NotificationNameExtension.swift in Sources */, + 87115F052756696F00601770 /* GetRouteListUsable.swift in Sources */, + 87115F1327566AB200601770 /* FavoriteItemDTO.swift in Sources */, + 87115F00275667A600601770 /* BaseUseCase.swift in Sources */, + 87115F01275668E100601770 /* BusCongestion.swift in Sources */, + 87115F062756698D00601770 /* BBusAPIUseCases.swift in Sources */, + 87115EFF27565DC200601770 /* BusPosByRtidDTO.swift in Sources */, + 87115F0D27566A7800601770 /* ServiceFetchable.swift in Sources */, + 87115F0C27566A7000601770 /* GetStationsByRouteListFetcher.swift in Sources */, + 87115F0827566A5200601770 /* TokenManager.swift in Sources */, + 87115F1227566AA700601770 /* PersistencetFetchable.swift in Sources */, + 87115F032756693700601770 /* BusPosByVehicleIdDTO.swift in Sources */, + 87115EFE27565DAE00601770 /* StationByRouteListDTO.swift in Sources */, + 875483782756810400136F16 /* BusRouteAPIUsable.swift in Sources */, + 87115F0927566A5900601770 /* NetworkService.swift in Sources */, + 87115EFD27565D9D00601770 /* BusRouteDTO.swift in Sources */, + 87115F0727566A4400601770 /* GetStationsByRouteListUsable.swift in Sources */, + 87115EFC27565BAD00601770 /* BusRouteAPIUseCase.swift in Sources */, + 87115EFB27565A3300601770 /* BusRouteViewModel.swift in Sources */, + 87115F0A27566A6200601770 /* PersistenceStorage.swift in Sources */, + 87115F1127566A9F00601770 /* BBusAPIError.swift in Sources */, + 4AF1E0C32756269F00DE51C8 /* BusRouteViewModelTests.swift in Sources */, + 87115F0E27566A8000601770 /* GetBusPosByRtidUsable.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0C9275626A700DE51C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 04DEBDF82757366C00B53D5F /* AlarmSettingBusArriveInfos.swift in Sources */, + 04DEBDF72757365C00B53D5F /* AverageSectionTimeCalculatable.swift in Sources */, + 04DEBDEE27572E8400B53D5F /* BaseUseCase.swift in Sources */, + 04DEBDFC275736D700B53D5F /* BusRemainTime.swift in Sources */, + 04DEBDF027572E8D00B53D5F /* JsonDTO.swift in Sources */, + 04DEBDF32757353800B53D5F /* AlarmSettingViewModel.swift in Sources */, + 4AF1E0D0275626A700DE51C8 /* AlarmSettingViewModelTests.swift in Sources */, + 04DEBDFA275736B500B53D5F /* PublisherExtension.swift in Sources */, + 04DEBDF42757355D00B53D5F /* BusRouteDTO.swift in Sources */, + 04DEBDEC27572E4700B53D5F /* AlarmSettingAPIUsable.swift in Sources */, + 04DEBDFD275736DE00B53D5F /* BusCongestion.swift in Sources */, + 04DEBDF5275735D900B53D5F /* AlarmSettingCalculateUseCase.swift in Sources */, + 04DEBDF9275736AB00B53D5F /* NotificationNameExtension.swift in Sources */, + 04DEBDF62757364D00B53D5F /* AlarmSettingCalculatable.swift in Sources */, + 04DEBDED27572E6400B53D5F /* ArrInfoByRouteDTO.swift in Sources */, + 04DEBDFE275736EE00B53D5F /* BBusAPIError.swift in Sources */, + 04DEBDFB275736C200B53D5F /* AlarmSettingBusArriveInfo.swift in Sources */, + 04DEBDEF27572E8800B53D5F /* StationByRouteListDTO.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4AF1E0D6275626B300DE51C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8707600227569254005A1E37 /* MovingStatusCalculatable.swift in Sources */, + 8707600827569324005A1E37 /* JsonDTO.swift in Sources */, + 870760092756932E005A1E37 /* PublisherExtension.swift in Sources */, + 8707600327569254005A1E37 /* MovingStatusCalculateUseCase.swift in Sources */, + 8707600127569160005A1E37 /* BusPosByRtidDTO.swift in Sources */, + 8707600027569158005A1E37 /* StationByRouteListDTO.swift in Sources */, + 8707600527569303005A1E37 /* AverageSectionTimeCalculatable.swift in Sources */, + 87075FFF2756914E005A1E37 /* BusRouteDTO.swift in Sources */, + 87075FFE275690E0005A1E37 /* MovingStatusAPIUsable.swift in Sources */, + 8707600727569319005A1E37 /* NotificationNameExtension.swift in Sources */, + 87075FFD27569066005A1E37 /* MovingStatusViewModel.swift in Sources */, + 8707600627569309005A1E37 /* BaseUseCase.swift in Sources */, + 4AF1E0DD275626B300DE51C8 /* MovingStatusViewModelTests.swift in Sources */, + 8707600A27569337005A1E37 /* BBusAPIError.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 87A555642728116400A9B5E3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4A5182B527551EC2001EA530 /* GetStationByUidItemUsable.swift in Sources */, + 0446843727567777007E440A /* AlarmSettingCalculatable.swift in Sources */, + 4ACA5231272FCEE600EC0531 /* MovingStatusView.swift in Sources */, + 4ADB29D7274E198900554A4E /* GetOffAlarmViewModel.swift in Sources */, + 4ACA51EF272FCDE600EC0531 /* StationSearchResult.swift in Sources */, + 4A17CED92733B5C6007B1646 /* SearchResultScrollView.swift in Sources */, + 4A5182B727551ED8001EA530 /* GetRouteListUsable.swift in Sources */, + 4ACA523B272FCF3C00EC0531 /* AlarmSettingViewController.swift in Sources */, + 04468439275677C0007E440A /* MovingStatusAPIUsable.swift in Sources */, + 4ACA5221272FCEAF00EC0531 /* AlarmSettingAPIUseCase.swift in Sources */, + 4ACA51F1272FCDF200EC0531 /* SearchAPIUseCase.swift in Sources */, + 4A094CDF27435C5900428F55 /* BusRemainTime.swift in Sources */, + 4AA294B5273C11DE008E5497 /* CreateFavoriteItemFetcher.swift in Sources */, + 4ACA5239272FCF3200EC0531 /* StationViewController.swift in Sources */, + 873D6396273039E100E79069 /* Coordinator.swift in Sources */, + 4A5100332754CDB100754B36 /* BaseUseCase.swift in Sources */, + 4AF1E0602755BE2400DE51C8 /* NavigatableView.swift in Sources */, + 4ADB29C9274B7AA200554A4E /* GetOnAlarmAPIUseCase.swift in Sources */, + 0446843B275677CA007E440A /* MovingStatusCalculatable.swift in Sources */, + 0476ABBD27311C1600F72DD1 /* BusStationTableViewCell.swift in Sources */, + 4A38E20727463FF7003A9D10 /* BBusAPIError.swift in Sources */, + 4A5182C127551F6D001EA530 /* DeleteFavoriteItemUsable.swift in Sources */, + 0446842D27567660007E440A /* HomeCalculatable.swift in Sources */, + 87038A8C273B96DF0078EAE3 /* PersistenceStorage.swift in Sources */, + 0446843527567769007E440A /* AlarmSettingAPIUsable.swift in Sources */, + 4ADB29CF274B890800554A4E /* BusApproachStatus.swift in Sources */, + 0476ABBB27310ED200F72DD1 /* BusRouteHeaderView.swift in Sources */, + 875F1AB22755BE08003F5BB1 /* AlarmSettingCalculateUseCase.swift in Sources */, + 04468427275675F1007E440A /* SearchAPIUsable.swift in Sources */, + 4AA294B7273C1275008E5497 /* DeleteFavoriteItemFetchable.swift in Sources */, + 4ACA51F5272FCE0200EC0531 /* SearchView.swift in Sources */, + 04C6D66B2734BCAB00D41678 /* MovingStatusBusTagView.swift in Sources */, + 049375B9273BE2640061ACDA /* StationByUidItemDTO.swift in Sources */, + 4ADB29CB274B7AB900554A4E /* GetOnAlarmViewModel.swift in Sources */, + 4ADB29CD274B880C00554A4E /* GetOnAlarmCalculateUsecase.swift in Sources */, + 4ACA520D272FCE6500EC0531 /* BusRouteView.swift in Sources */, + 4ADB29B9274B68CE00554A4E /* GetOnAlarmController.swift in Sources */, + 87038A88273B950B0078EAE3 /* GetArrInfoByRouteListFetcher.swift in Sources */, + 87038A82273B90A50078EAE3 /* BBusAPIUseCases.swift in Sources */, + 4ACA51E3272FCD9600EC0531 /* HomeModel.swift in Sources */, + 87115F172756758800601770 /* GetArrInfoByRouteListUseCase.swift in Sources */, + 4ACA51E9272FCDAE00EC0531 /* HomeView.swift in Sources */, + 4ADB29D4274E18BB00554A4E /* GetOffAlarmController.swift in Sources */, + 4A06AEC8274159D10027222D /* FavoriteItemDTO.swift in Sources */, + 4ACA521F272FCEAA00EC0531 /* AlarmSettingBusArriveInfo.swift in Sources */, 4A1A22DB27326FD100476861 /* HomeNavigationView.swift in Sources */, + 4A5182A42754E3CA001EA530 /* TokenManager.swift in Sources */, 4AC79161274F6FDB00019827 /* SourceFooterView.swift in Sources */, + 4A5182B327551EA9001EA530 /* GetBusPosByRtidUsable.swift in Sources */, 0484107627464D49006F8636 /* AlarmSettingBusArriveInfos.swift in Sources */, + 4A51003B2755180400754B36 /* RefreshButton.swift in Sources */, 4ACA5225272FCEBF00EC0531 /* AlarmSettingView.swift in Sources */, 87038A92273C12320078EAE3 /* GetBusPosByRtidFetcher.swift in Sources */, + 4A510039275517F800754B36 /* RefreshableView.swift in Sources */, + 87115F1D2756761200601770 /* GetStationByUidItemUseCase.swift in Sources */, 4A094D072743E12C00428F55 /* NoneInfoTableViewCell.swift in Sources */, 4ACA51E7272FCDA600EC0531 /* HomeViewModel.swift in Sources */, + 87115F19275675B900601770 /* GetStationsByRouteListUseCase.swift in Sources */, + 87115F1B275675E200601770 /* GetBusPosByRtidUseCase.swift in Sources */, 4A1A22DD2732801700476861 /* StationCoordinator.swift in Sources */, 4ACA5237272FCF2C00EC0531 /* BusRouteViewController.swift in Sources */, 873D639827303A6800E79069 /* AppCoordinator.swift in Sources */, 4ACA5217272FCE8E00EC0531 /* StationViewModel.swift in Sources */, + 044684292756760A007E440A /* SearchCalculatable.swift in Sources */, 4A38E20927464015003A9D10 /* PublisherExtension.swift in Sources */, 047C30FC273A04C20075EA14 /* BBusColor.swift in Sources */, 87038A94273C12E20078EAE3 /* GetStationByUidItemFetcher.swift in Sources */, 4ACA51F3272FCDF900EC0531 /* SearchViewModel.swift in Sources */, 049375AB273B9F330061ACDA /* BusRouteDTO.swift in Sources */, + 875F1AB02755BD86003F5BB1 /* AverageSectionTimeCalculatable.swift in Sources */, 87A555702728116400A9B5E3 /* HomeViewController.swift in Sources */, 04214061273A5EBC00A15423 /* MovingStatusFoldUnfoldDelegate.swift in Sources */, 4A992C4727311F1C0006FB8C /* FavoriteCollectionHeaderView.swift in Sources */, + 0446842B27567654007E440A /* HomeAPIUsable.swift in Sources */, 4ADB29BD274B6E0B00554A4E /* GetBusPosByVehIdFetcher.swift in Sources */, 049375B7273BE08F0061ACDA /* BusPosByRtidDTO.swift in Sources */, + 4A5182BB27551F28001EA530 /* GetStationListUsable.swift in Sources */, 4A992C49273124AF0006FB8C /* FavoriteCollectionViewCell.swift in Sources */, 4ADB29C7274B7A7F00554A4E /* GetOnAlarmStatus.swift in Sources */, + 87115F27275676C500601770 /* CreateFavoriteItemUseCase.swift in Sources */, 4ACC26B4274F669800173A32 /* BusSectionKeys.swift in Sources */, 4A917DF227462E36002489FE /* EmptyFavoriteNoticeView.swift in Sources */, 04AC7D1B2733E7270095CD4E /* AlarmSettingButton.swift in Sources */, @@ -997,14 +2062,16 @@ 049375BD273C2E120061ACDA /* ArrInfoByRouteDTO.swift in Sources */, 873D639A27303B0500E79069 /* HomeCoordinator.swift in Sources */, 04045B7F2740F6B30056A433 /* SearchResults.swift in Sources */, - 4ACA5215272FCE8A00EC0531 /* StationUseCase.swift in Sources */, + 4ACA5215272FCE8A00EC0531 /* StationAPIUseCase.swift in Sources */, 4ACA5213272FCE8500EC0531 /* BusCongestion.swift in Sources */, 04AC7D112733AF090095CD4E /* BusRouteTableViewCell.swift in Sources */, 04045B812740F6DC0056A433 /* BusSearchResult.swift in Sources */, 041A5A11273D2E1B00490075 /* StringExtension.swift in Sources */, + 4A5182B927551F06001EA530 /* GetBusPosByVehIdUsable.swift in Sources */, 87038A8E273C10480078EAE3 /* GetRouteInfoItemFetcher.swift in Sources */, 4ACA523D272FCF4300EC0531 /* MovingStatusViewController.swift in Sources */, 049375AF273BAA130061ACDA /* StationDTO.swift in Sources */, + 4A5182AF27551E3C001EA530 /* GetArrInfoByRouteListUsable.swift in Sources */, 4A992C4B273124D90006FB8C /* BusCellTrailingView.swift in Sources */, 4ACA5223272FCEB500EC0531 /* AlarmSettingViewModel.swift in Sources */, 873578832732545D00CC8ECC /* CustomNavigationBar.swift in Sources */, @@ -1013,8 +2080,12 @@ 4A04682A2732B7B3008D87CE /* SearchNavigationView.swift in Sources */, 047C30FA273A04B40075EA14 /* BBusImage.swift in Sources */, 4ACA5219272FCE9500EC0531 /* StationView.swift in Sources */, + 0446843D275679B9007E440A /* JsonDTO.swift in Sources */, + 4A5182BF27551F57001EA530 /* CreateFavoriteItemUsable.swift in Sources */, 4A1A22E12732CE7900476861 /* SearchResultCollectionViewCell.swift in Sources */, 87EB52AE2733A6C6000D5492 /* GetOnStatusCell.swift in Sources */, + 04DC47FB27552FE5003380D9 /* StationCalculateUseCase.swift in Sources */, + 87115F29275676E900601770 /* DeleteFavoriteItemUseCase.swift in Sources */, 4A06AEE92743DAB20027222D /* NotificationNameExtension.swift in Sources */, 4ADB29BB274B6C8300554A4E /* BusPosByVehicleIdDTO.swift in Sources */, 4ACC26B8274F6BD300173A32 /* BusArriveInfos.swift in Sources */, @@ -1026,71 +2097,521 @@ 4A094CE127438EB800428F55 /* UIViewExtension.swift in Sources */, 4A7BBFF12737D6C20029915F /* RemainCongestionBadgeLabel.swift in Sources */, 04AC7D132733B0940095CD4E /* GetOffTableViewCell.swift in Sources */, - 4ACA522D272FCEDB00EC0531 /* MovingStatusUseCase.swift in Sources */, + 4ACA522D272FCEDB00EC0531 /* MovingStatusAPIUseCase.swift in Sources */, 4AA294AF273C0E8D008E5497 /* GetRouteListFetcher.swift in Sources */, 4A04682C2732ECAA008D87CE /* KeyboardAccessoryView.swift in Sources */, - 4ACA5209272FCE5A00EC0531 /* BusRouteUseCase.swift in Sources */, + 87115F1F2756766900601770 /* GetBusPosByVehIdUseCase.swift in Sources */, + 4A5182A627550988001EA530 /* RequestFactory.swift in Sources */, + 4ACA5209272FCE5A00EC0531 /* BusRouteAPIUseCase.swift in Sources */, + 0446842F275676B3007E440A /* BusRouteAPIUsable.swift in Sources */, + 4A5100372755106A00754B36 /* HomeCalculateUseCase.swift in Sources */, 4AA294B1273C1094008E5497 /* GetStationListFetcher.swift in Sources */, 4ACA5233272FCF0F00EC0531 /* SearchViewController.swift in Sources */, - 87038A8A273B96B60078EAE3 /* Service.swift in Sources */, + 04468433275676F8007E440A /* StationCalculatable.swift in Sources */, + 87038A8A273B96B60078EAE3 /* NetworkService.swift in Sources */, + 87B35D0A2754E71F00159791 /* MovingStatusCalculateUseCase.swift in Sources */, 4A412028274E34430083D691 /* BusRouteModel.swift in Sources */, 87A5556C2728116400A9B5E3 /* AppDelegate.swift in Sources */, + 4A5182A8275511D5001EA530 /* ServiceFetchable.swift in Sources */, + 4A5100352754CE1500754B36 /* BaseViewController.swift in Sources */, 87038A90273C11630078EAE3 /* GetStationsByRouteListFetcher.swift in Sources */, 4ACA522B272FCED600EC0531 /* MovingStatusModel.swift in Sources */, + 0451EDB42755BBBD00031A16 /* SearchCalculateUseCase.swift in Sources */, 4A917DF427463666002489FE /* EmptySearchResultNoticeView.swift in Sources */, + 4A5182BD27551F40001EA530 /* GetFavoriteItemListUsable.swift in Sources */, + 87115F25275676B000601770 /* GetFavoriteItemListUseCase.swift in Sources */, 4ADB29D1274CAF0100554A4E /* AlarmStartResult.swift in Sources */, + 4A5182B127551E84001EA530 /* GetStationsByRouteListUsable.swift in Sources */, + 4A5182AC275516A4001EA530 /* PersistencetFetchable.swift in Sources */, 87A5556E2728116400A9B5E3 /* SceneDelegate.swift in Sources */, 04C6D6692734B18A00D41678 /* MovingStatusTableViewCell.swift in Sources */, + 87115F232756769D00601770 /* GetStationListUseCase.swift in Sources */, + 87115F212756768900601770 /* GetRouteListUseCase.swift in Sources */, 87359B8D27311BF100F461A7 /* BusTagView.swift in Sources */, + 4A3B6C502755DF2400BBC498 /* AlarmCenter.swift in Sources */, 4A17CED72733A754007B1646 /* SimpleCollectionHeaderView.swift in Sources */, 047C30FE273A05A60075EA14 /* BBusRouteType.swift in Sources */, - 4ACA51E5272FCD9C00EC0531 /* HomeUseCase.swift in Sources */, - 87038A85273B90D10078EAE3 /* RequestUsecases.swift in Sources */, + 4ACA51E5272FCD9C00EC0531 /* HomeAPIUseCase.swift in Sources */, + 4ACA51E5272FCD9C00EC0531 /* HomeAPIUseCase.swift in Sources */, 87285F5527461DB300CA3BA9 /* UINavigationControllerExtension.swift in Sources */, 4A7BBFEF27378AA50029915F /* StationBodyCollectionViewCell.swift in Sources */, 4ACA522F272FCEDF00EC0531 /* MovingStatusViewModel.swift in Sources */, + 04468431275676ED007E440A /* StationAPIUsable.swift in Sources */, 4AA294B3273C111C008E5497 /* GetFavoriteItemListFetcher.swift in Sources */, 4ACA520B272FCE5F00EC0531 /* BusRouteViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 4ACA5247272FD52D00EC0531 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 87A555672728116400A9B5E3 /* BBus */; - targetProxy = 4ACA5246272FD52D00EC0531 /* PBXContainerItemProxy */; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 4ACA5247272FD52D00EC0531 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4ACA5246272FD52D00EC0531 /* PBXContainerItemProxy */; + }; + 4AF1E06A2756262300DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E0692756262300DE51C8 /* PBXContainerItemProxy */; + }; + 4AF1E0772756263400DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E0762756263400DE51C8 /* PBXContainerItemProxy */; + }; + 4AF1E0842756263E00DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E0832756263E00DE51C8 /* PBXContainerItemProxy */; + }; + 4AF1E0912756264600DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E0902756264600DE51C8 /* PBXContainerItemProxy */; + }; + 4AF1E09E2756266B00DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E09D2756266B00DE51C8 /* PBXContainerItemProxy */; + }; + 4AF1E0AB2756268B00DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E0AA2756268B00DE51C8 /* PBXContainerItemProxy */; + }; + 4AF1E0B82756269600DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E0B72756269600DE51C8 /* PBXContainerItemProxy */; + }; + 4AF1E0C52756269F00DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E0C42756269F00DE51C8 /* PBXContainerItemProxy */; + }; + 4AF1E0D2275626A700DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E0D1275626A700DE51C8 /* PBXContainerItemProxy */; + }; + 4AF1E0DF275626B300DE51C8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87A555672728116400A9B5E3 /* BBus */; + targetProxy = 4AF1E0DE275626B300DE51C8 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 87A555762728116600A9B5E3 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 87A555772728116600A9B5E3 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4ACA5249272FD52D00EC0531 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = NJ72J269P7; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.BBus.BBusTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; + }; + 4ACA524A272FD52D00EC0531 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = NJ72J269P7; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.BBus.BBusTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; + }; + 4AF1E06B2756262300DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.NetworkServiceTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; + }; + 4AF1E06C2756262300DE51C8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.NetworkServiceTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; + }; + 4AF1E0792756263400DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.PersistenceStorageTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; + }; + 4AF1E07A2756263400DE51C8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.PersistenceStorageTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; + }; + 4AF1E0862756263E00DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.TokenManagerTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; + }; + 4AF1E0872756263E00DE51C8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.TokenManagerTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; + }; + 4AF1E0932756264600DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.RequestFactoryTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; + }; + 4AF1E0942756264600DE51C8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.RequestFactoryTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; + }; + 4AF1E0A02756266B00DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.HomeViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; + }; + 4AF1E0A12756266B00DE51C8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.HomeViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; + }; + 4AF1E0AD2756268B00DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.SearchViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; + }; + 4AF1E0AE2756268B00DE51C8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.SearchViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 87A555762728116600A9B5E3 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 87A555772728116600A9B5E3 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; + 4AF1E0BA2756269600DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.StationViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 4ACA5249272FD52D00EC0531 /* Debug */ = { + 4AF1E0BB2756269600DE51C8 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJ72J269P7; + DEVELOPMENT_TEAM = B3PWYBKFUK; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.BBus.BBusTests"; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.StationViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; + }; + 4AF1E0C72756269F00DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.BusRouteViewModelTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -1099,21 +2620,118 @@ }; name = Debug; }; - 4ACA524A272FD52D00EC0531 /* Release */ = { + 4AF1E0C82756269F00DE51C8 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJ72J269P7; + DEVELOPMENT_TEAM = B3PWYBKFUK; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.BBus.BBusTests"; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.BusRouteViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; + }; + 4AF1E0D4275626A700DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.AlarmSettingViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; + }; + 4AF1E0D5275626A700DE51C8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.AlarmSettingViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Release; + }; + 4AF1E0E1275626B300DE51C8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.MovingStatusViewModelTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BBus.app/BBus"; + }; + name = Debug; + }; + 4AF1E0E2275626B300DE51C8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = B3PWYBKFUK; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.boostcamp.ios-009.MovingStatusViewModelTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -1249,11 +2867,14 @@ DEVELOPMENT_TEAM = B3PWYBKFUK; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BBus/Info.plist; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "하차 알람을 사용하기 위해서는 위치 정보가 필요합니다."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "알람을 위해 앱을 사용중이지 않을 때도 사용자의 GPS 위치를 추적해야합니다."; + INFOPLIST_KEY_NSLocationUsageDescription = "하차 알람을 위해서 사용자의 GPS 위치를 추적해야합니다."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "알람을 위해 사용자의 GPS 위치를 추적해야합니다."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1279,11 +2900,14 @@ DEVELOPMENT_TEAM = B3PWYBKFUK; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BBus/Info.plist; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "하차 알람을 사용하기 위해서는 위치 정보가 필요합니다."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "알람을 위해 앱을 사용중이지 않을 때도 사용자의 GPS 위치를 추적해야합니다."; + INFOPLIST_KEY_NSLocationUsageDescription = "하차 알람을 위해서 사용자의 GPS 위치를 추적해야합니다."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "알람을 위해 사용자의 GPS 위치를 추적해야합니다."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1310,6 +2934,96 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 4AF1E06D2756262300DE51C8 /* Build configuration list for PBXNativeTarget "NetworkServiceTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E06B2756262300DE51C8 /* Debug */, + 4AF1E06C2756262300DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AF1E0782756263400DE51C8 /* Build configuration list for PBXNativeTarget "PersistenceStorageTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E0792756263400DE51C8 /* Debug */, + 4AF1E07A2756263400DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AF1E0852756263E00DE51C8 /* Build configuration list for PBXNativeTarget "TokenManagerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E0862756263E00DE51C8 /* Debug */, + 4AF1E0872756263E00DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AF1E0922756264600DE51C8 /* Build configuration list for PBXNativeTarget "RequestFactoryTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E0932756264600DE51C8 /* Debug */, + 4AF1E0942756264600DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AF1E09F2756266B00DE51C8 /* Build configuration list for PBXNativeTarget "HomeViewModelTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E0A02756266B00DE51C8 /* Debug */, + 4AF1E0A12756266B00DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AF1E0AC2756268B00DE51C8 /* Build configuration list for PBXNativeTarget "SearchViewModelTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E0AD2756268B00DE51C8 /* Debug */, + 4AF1E0AE2756268B00DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AF1E0B92756269600DE51C8 /* Build configuration list for PBXNativeTarget "StationViewModelTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E0BA2756269600DE51C8 /* Debug */, + 4AF1E0BB2756269600DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AF1E0C62756269F00DE51C8 /* Build configuration list for PBXNativeTarget "BusRouteViewModelTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E0C72756269F00DE51C8 /* Debug */, + 4AF1E0C82756269F00DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AF1E0D3275626A700DE51C8 /* Build configuration list for PBXNativeTarget "AlarmSettingViewModelTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E0D4275626A700DE51C8 /* Debug */, + 4AF1E0D5275626A700DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4AF1E0E0275626B300DE51C8 /* Build configuration list for PBXNativeTarget "MovingStatusViewModelTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4AF1E0E1275626B300DE51C8 /* Debug */, + 4AF1E0E2275626B300DE51C8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 87A555632728116400A9B5E3 /* Build configuration list for PBXProject "BBus" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/BBus/BBus.xcodeproj/xcshareddata/xcschemes/BBus.xcscheme b/BBus/BBus.xcodeproj/xcshareddata/xcschemes/BBus.xcscheme index fcb211d9..7da277d1 100644 --- a/BBus/BBus.xcodeproj/xcshareddata/xcschemes/BBus.xcscheme +++ b/BBus/BBus.xcodeproj/xcshareddata/xcschemes/BBus.xcscheme @@ -38,6 +38,106 @@ ReferencedContainer = "container:BBus.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AlarmStartResult { if let viewModel = viewModel { @@ -32,5 +36,9 @@ final class GetOffAlarmController: NSObject { func stop() { self.viewModel = nil } - + + func configureAlarmPermission(_ delegate: CLLocationManagerDelegate) { + self.alarmCenter.configurePermission() + self.alarmCenter.configureLocationDetail(delegate) + } } diff --git a/BBus/BBus/Background/GetOnAlarm/GetOnAlarmController.swift b/BBus/BBus/Background/GetOnAlarm/GetOnAlarmController.swift index 94a8544d..ff47de54 100644 --- a/BBus/BBus/Background/GetOnAlarm/GetOnAlarmController.swift +++ b/BBus/BBus/Background/GetOnAlarm/GetOnAlarmController.swift @@ -6,22 +6,21 @@ // import Foundation -import UIKit import Combine -import CoreLocation final class GetOnAlarmController { static private let alarmIdentifier: String = "GetOnAlarm" - static let shared = GetOnAlarmController() + static let shared = GetOnAlarmController(alarmCenter: AlarmCenter()) + private let alarmCenter: AlarmManagable private var cancellables: Set - private var locationManager: CLLocationManager? @Published private(set) var viewModel: GetOnAlarmViewModel? - private init() { + private init(alarmCenter: AlarmManagable) { + self.alarmCenter = alarmCenter self.cancellables = [] } @@ -35,33 +34,28 @@ final class GetOnAlarmController { } } else { - let usecase = GetOnAlarmUsecase(usecases: BBusAPIUsecases(on: GetOnAlarmUsecase.queue)) + let apiUseCases = BBusAPIUseCases(networkService: NetworkService(), + persistenceStorage: PersistenceStorage(), + tokenManageType: TokenManager.self, + requestFactory: RequestFactory()) + let useCase = GetOnAlarmAPIUseCase(useCases: apiUseCases) let getOnAlarmStatus = GetOnAlarmStatus(currentBusOrd: nil, targetOrd: targetOrd, vehicleId: vehicleId, busName: busName, busRouteId: busRouteId, stationId: stationId) - self.viewModel = GetOnAlarmViewModel(usecase: usecase, currentStatus: getOnAlarmStatus) + self.viewModel = GetOnAlarmViewModel(useCase: useCase, currentStatus: getOnAlarmStatus) self.viewModel?.fetch() self.binding() - self.sendRequestAuthorization() - self.configureLocationManager() + self.alarmCenter.configurePermission() return .success } } - + func stop() { self.viewModel = nil self.cancellables = [] - self.locationManager = nil - } - - private func configureLocationManager() { - self.locationManager = CLLocationManager() - self.locationManager?.requestAlwaysAuthorization() - self.locationManager?.allowsBackgroundLocationUpdates = true - self.locationManager?.startUpdatingLocation() } private func binding() { @@ -77,7 +71,9 @@ final class GetOnAlarmController { if status == .oneStationLeft { self?.stop() } - self?.pushGetOnAlarm(title: "승차 알람", message: message) + self?.alarmCenter.pushAlarm(in: Self.alarmIdentifier, + title: "승차 알람", + message: message) }) .store(in: &self.cancellables) } @@ -86,29 +82,13 @@ final class GetOnAlarmController { self.viewModel?.$networkErrorMessage .sink(receiveValue: { [weak self] content in guard let content = content else { return } - self?.pushGetOnAlarm(title: content.title, message: content.body) + self?.alarmCenter.pushAlarm(in: Self.alarmIdentifier, + title: content.title, + message: content.body) self?.stop() }) .store(in: &self.cancellables) } - - private func sendRequestAuthorization() { - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .sound, .badge], completionHandler: { didAllow, error in - if let error = error { - print(error) - } - }) - } - - private func pushGetOnAlarm(title: String, message: String) { - let content = UNMutableNotificationContent() - content.title = title - content.body = message - content.badge = Int(truncating: content.badge ?? 0) + 1 as NSNumber - let request = UNNotificationRequest(identifier: Self.alarmIdentifier, content: content, trigger: nil) - UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) - } private func isSameAlarm(targetOrd: Int, vehicleId: Int) -> Bool { guard let currentTargetOrd = self.viewModel?.getOnAlarmStatus.targetOrd, diff --git a/BBus/BBus/Background/GetOnAlarm/UseCase/BusApproachCheckUsecase.swift b/BBus/BBus/Background/GetOnAlarm/UseCase/BusApproachCheckUsecase.swift deleted file mode 100644 index a33f0175..00000000 --- a/BBus/BBus/Background/GetOnAlarm/UseCase/BusApproachCheckUsecase.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// BusApproachCheckUsecase.swift -// BBus -// -// Created by 김태훈 on 2021/11/22. -// - -import Foundation - -struct BusApproachCheckUsecase { - func execute(currentOrd: Int, beforeOrd: Int, targetOrd: Int) -> BusApproachStatus? { - guard currentOrd != beforeOrd else { return nil } - return BusApproachStatus(rawValue: targetOrd - currentOrd) - } -} diff --git a/BBus/BBus/Background/GetOnAlarm/UseCase/GetOnAlarmAPIUseCase.swift b/BBus/BBus/Background/GetOnAlarm/UseCase/GetOnAlarmAPIUseCase.swift new file mode 100644 index 00000000..b4d76e4c --- /dev/null +++ b/BBus/BBus/Background/GetOnAlarm/UseCase/GetOnAlarmAPIUseCase.swift @@ -0,0 +1,42 @@ +// +// GetOnAlarmUsecase.swift +// BBus +// +// Created by 김태훈 on 2021/11/22. +// + +import Foundation +import Combine + +protocol GetOnAlarmAPIUsable: BaseUseCase { + func fetch(withVehId vehId: String) +} + +final class GetOnAlarmAPIUseCase: GetOnAlarmAPIUsable { + + private let useCases: GetBusPosByVehIdUsable + private var cancellable: AnyCancellable? + @Published private(set) var networkError: Error? + @Published private(set) var busPosition: BusPosByVehicleIdDTO? + + init(useCases: GetBusPosByVehIdUsable) { + self.useCases = useCases + self.cancellable = nil + self.networkError = nil + self.busPosition = nil + } + + func fetch(withVehId vehId: String) { + self.cancellable = self.useCases.getBusPosByVehId(vehId) + .decode(type: JsonMessage.self, decoder: JSONDecoder()) + .retry({ [weak self] in + self?.fetch(withVehId: vehId) + }, handler: { [weak self] error in + self?.networkError = error + }) + .map({ item in + item.msgBody.itemList.first + }) + .assign(to: \.busPosition, on: self) + } +} diff --git a/BBus/BBus/Background/GetOnAlarm/UseCase/GetOnAlarmCalculateUsecase.swift b/BBus/BBus/Background/GetOnAlarm/UseCase/GetOnAlarmCalculateUsecase.swift new file mode 100644 index 00000000..d2c33c02 --- /dev/null +++ b/BBus/BBus/Background/GetOnAlarm/UseCase/GetOnAlarmCalculateUsecase.swift @@ -0,0 +1,20 @@ +// +// BusApproachCheckUsecase.swift +// BBus +// +// Created by 김태훈 on 2021/11/22. +// + +import Foundation + +protocol GetOnAlarmCalculatable: BaseUseCase { + func busApproachStatus(currentOrd: Int, beforeOrd: Int, targetOrd: Int) -> BusApproachStatus? +} + +struct GetOnAlarmCalculateUsecase: GetOnAlarmCalculatable { + + func busApproachStatus(currentOrd: Int, beforeOrd: Int, targetOrd: Int) -> BusApproachStatus? { + guard currentOrd != beforeOrd else { return nil } + return BusApproachStatus(rawValue: targetOrd - currentOrd) + } +} diff --git a/BBus/BBus/Background/GetOnAlarm/UseCase/GetOnAlarmUsecase.swift b/BBus/BBus/Background/GetOnAlarm/UseCase/GetOnAlarmUsecase.swift deleted file mode 100644 index 6e195b96..00000000 --- a/BBus/BBus/Background/GetOnAlarm/UseCase/GetOnAlarmUsecase.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// GetOnAlarmUsecase.swift -// BBus -// -// Created by 김태훈 on 2021/11/22. -// - -import Foundation -import Combine - -final class GetOnAlarmUsecase { - - private let usecases: GetBusPosByVehIdUsecase - private var cancellable: AnyCancellable? - static let queue = DispatchQueue.init(label: "GetOnAlarm") - @Published private(set) var networkError: Error? - @Published private(set) var busPosition: BusPosByVehicleIdDTO? - - init(usecases: GetBusPosByVehIdUsecase) { - self.usecases = usecases - self.cancellable = nil - self.networkError = nil - self.busPosition = nil - } - - func fetch(withVehId vehId: String) { - - Self.queue.async { - self.cancellable = self.usecases.getBusPosByVehId(vehId) - .receive(on: Self.queue) - .decode(type: JsonMessage.self, decoder: JSONDecoder()) - .retry({ [weak self] in - self?.fetch(withVehId: vehId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .map({ item in - item.msgBody.itemList.first - }) - .assign(to: \.busPosition, on: self) - - } - } -} diff --git a/BBus/BBus/Background/GetOnAlarm/ViewModel/GetOnAlarmViewModel.swift b/BBus/BBus/Background/GetOnAlarm/ViewModel/GetOnAlarmViewModel.swift index 4400750d..29fadd1b 100644 --- a/BBus/BBus/Background/GetOnAlarm/ViewModel/GetOnAlarmViewModel.swift +++ b/BBus/BBus/Background/GetOnAlarm/ViewModel/GetOnAlarmViewModel.swift @@ -10,15 +10,15 @@ import Combine final class GetOnAlarmViewModel { - let usecase: GetOnAlarmUsecase + private let useCase: GetOnAlarmAPIUseCase private(set) var getOnAlarmStatus: GetOnAlarmStatus private var cancellables: Set @Published private(set) var busApproachStatus: BusApproachStatus? private(set) var message: String? @Published private(set) var networkErrorMessage: (title: String, body: String)? - init(usecase: GetOnAlarmUsecase, currentStatus: GetOnAlarmStatus) { - self.usecase = usecase + init(useCase: GetOnAlarmAPIUseCase, currentStatus: GetOnAlarmStatus) { + self.useCase = useCase self.getOnAlarmStatus = currentStatus self.message = nil self.networkErrorMessage = nil @@ -40,14 +40,14 @@ final class GetOnAlarmViewModel { } func bindBusPosition() { - self.usecase.$busPosition - .receive(on: GetOnAlarmUsecase.queue) + self.useCase.$busPosition + .receive(on: DispatchQueue.global()) .sink { [weak self] position in guard let self = self, let position = position, let stationOrd = Int(position.stationOrd) else { return } - if let status = BusApproachCheckUsecase().execute(currentOrd: stationOrd, + if let status = GetOnAlarmCalculateUsecase().busApproachStatus(currentOrd: stationOrd, beforeOrd: self.getOnAlarmStatus.currentBusOrd ?? stationOrd, targetOrd: self.getOnAlarmStatus.targetOrd) { self.makeMessage(with: status) @@ -59,8 +59,8 @@ final class GetOnAlarmViewModel { } func bindNetworkErrorMessage() { - self.usecase.$networkError - .receive(on: GetOnAlarmUsecase.queue) + self.useCase.$networkError + .receive(on: DispatchQueue.global()) .sink(receiveValue: { [weak self] error in guard let _ = error else { return } self?.networkErrorMessage = ("승차 알람", "네트워크 에러가 발생하여 알람이 취소됩니다.") @@ -69,7 +69,7 @@ final class GetOnAlarmViewModel { } func fetch() { - self.usecase.fetch(withVehId: "\(self.getOnAlarmStatus.vehicleId)") + self.useCase.fetch(withVehId: "\(self.getOnAlarmStatus.vehicleId)") } private func makeMessage(with status: BusApproachStatus) { diff --git a/BBus/BBus/Foreground/AlarmSetting/AlarmSettingCoordinator.swift b/BBus/BBus/Foreground/AlarmSetting/AlarmSettingCoordinator.swift index 98cd0705..7154c385 100644 --- a/BBus/BBus/Foreground/AlarmSetting/AlarmSettingCoordinator.swift +++ b/BBus/BBus/Foreground/AlarmSetting/AlarmSettingCoordinator.swift @@ -17,8 +17,14 @@ final class AlarmSettingCoordinator: Coordinator { } func start(stationId: Int, busRouteId: Int, stationOrd: Int, arsId: String, routeType: RouteType?, busName: String) { - let useCase = AlarmSettingUseCase(useCases: BBusAPIUsecases(on: AlarmSettingUseCase.queue)) - let viewModel = AlarmSettingViewModel(useCase: useCase, + let apiUseCases = BBusAPIUseCases(networkService: NetworkService(), + persistenceStorage: PersistenceStorage(), + tokenManageType: TokenManager.self, + requestFactory: RequestFactory()) + let apiUseCase = AlarmSettingAPIUseCase(useCases: apiUseCases) + let calculateUseCase = AlarmSettingCalculateUseCase() + let viewModel = AlarmSettingViewModel(apiUseCase: apiUseCase, + calculateUseCase: calculateUseCase, stationId: stationId, busRouteId: busRouteId, stationOrd: stationOrd, diff --git a/BBus/BBus/Foreground/AlarmSetting/AlarmSettingViewController.swift b/BBus/BBus/Foreground/AlarmSetting/AlarmSettingViewController.swift index dc720b92..17328754 100644 --- a/BBus/BBus/Foreground/AlarmSetting/AlarmSettingViewController.swift +++ b/BBus/BBus/Foreground/AlarmSetting/AlarmSettingViewController.swift @@ -8,25 +8,12 @@ import UIKit import Combine -final class AlarmSettingViewController: UIViewController { - +final class AlarmSettingViewController: UIViewController, BaseViewControllerType { + weak var coordinator: AlarmSettingCoordinator? - private lazy var alarmSettingView = AlarmSettingView() - private lazy var customNavigationBar = CustomNavigationBar() - private lazy var refreshButton: ThrottleButton = { - let radius: CGFloat = 25 - - let button = ThrottleButton() - button.setImage(BBusImage.refresh, for: .normal) - button.layer.cornerRadius = radius - button.tintColor = BBusColor.white - button.backgroundColor = BBusColor.darkGray - button.addTouchUpEventWithThrottle(delay: ThrottleButton.refreshInterval) { [weak self] in - self?.viewModel?.refresh() - } - return button - }() private let viewModel: AlarmSettingViewModel? + private lazy var alarmSettingView = AlarmSettingView() + private var cancellables: Set = [] init(viewModel: AlarmSettingViewModel) { @@ -41,18 +28,16 @@ final class AlarmSettingViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + self.baseViewDidLoad() self.configureColor() - self.configureLayout() - self.configureDelegate() - - self.binding() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.alarmSettingView.startLoader() self.viewModel?.configureObserver() + self.viewModel?.activateLoaderActiveStatus() self.viewModel?.refresh() } @@ -62,48 +47,31 @@ final class AlarmSettingViewController: UIViewController { } // MARK: - Configure - private func configureLayout() { - let refreshButtonWidthAnchor: CGFloat = 50 - let refreshTrailingBottomInterval: CGFloat = -16 - - self.view.addSubviews(self.customNavigationBar, self.alarmSettingView, self.refreshButton) + func configureLayout() { + self.view.addSubviews(self.alarmSettingView) NSLayoutConstraint.activate([ - self.customNavigationBar.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), - self.customNavigationBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), - self.customNavigationBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) - ]) - - NSLayoutConstraint.activate([ - self.alarmSettingView.topAnchor.constraint(equalTo: self.customNavigationBar.bottomAnchor), + self.alarmSettingView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), self.alarmSettingView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), self.alarmSettingView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), self.alarmSettingView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) ]) - - NSLayoutConstraint.activate([ - self.refreshButton.widthAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), - self.refreshButton.heightAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), - self.refreshButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: refreshTrailingBottomInterval), - self.refreshButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: refreshTrailingBottomInterval) - ]) } - private func configureDelegate() { + func configureDelegate() { self.alarmSettingView.configureDelegate(self) - self.customNavigationBar.configureDelegate(self) } private func configureColor() { self.view.backgroundColor = BBusColor.white - self.customNavigationBar.configureTintColor(color: BBusColor.black) - self.customNavigationBar.configureAlpha(alpha: 1) + self.alarmSettingView.configureColor(color: BBusColor.black) } - private func binding() { + func bindAll() { self.bindBusArriveInfos() self.bindBusStationInfos() self.bindErrorMessage() + self.bindLoaderActiveStatus() } private func bindBusArriveInfos() { @@ -111,35 +79,24 @@ final class AlarmSettingViewController: UIViewController { .filter { !$0.changedByTimer } .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) .sink(receiveValue: { [weak self] _ in - guard let viewModel = self?.viewModel else { return } - self?.alarmSettingView.reload() - - if viewModel.isStopLoader() { - self?.alarmSettingView.stopLoader() - } }) .store(in: &self.cancellables) } private func bindBusStationInfos() { self.viewModel?.$busStationInfos + .dropFirst() .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] infos in - guard let viewModel = self?.viewModel else { return } - if let infos = infos { self?.alarmSettingView.reload() if let viewModel = self?.viewModel, let stationName = infos.first?.name { - self?.customNavigationBar.configureTitle(busName: viewModel.busName, + self?.alarmSettingView.configureTitle(busName: viewModel.busName, stationName: stationName, routeType: viewModel.routeType) } - - if viewModel.isStopLoader() { - self?.alarmSettingView.stopLoader() - } } else { self?.noInfoAlert() @@ -149,23 +106,30 @@ final class AlarmSettingViewController: UIViewController { } private func bindErrorMessage() { - self.viewModel?.$errorMessage + self.viewModel?.$networkError + .compactMap({$0}) .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] message in - guard let message = message else { return } - self?.alarmSettingAlert(message: message) + .sink(receiveValue: { [weak self] _ in + self?.networkAlert() }) .store(in: &self.cancellables) - - self.viewModel?.useCase.$networkError + } + + private func bindLoaderActiveStatus() { + self.viewModel?.$loaderActiveStatus + .dropFirst() + .filter({ !$0 }) .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] error in - guard let _ = error else { return } - self?.networkAlert() + .sink(receiveValue: { [weak self] a in + self?.alarmSettingView.stopLoader() }) .store(in: &self.cancellables) } + func refresh() { + self.viewModel?.refresh() + } + private func alarmSettingAlert(message: String) { let controller = UIAlertController() let action = UIAlertAction(title: message, style: .cancel, handler: nil) @@ -348,6 +312,13 @@ extension AlarmSettingViewController: BackButtonDelegate { } } +// MARK: - Delegate: RefreshButton +extension AlarmSettingViewController: RefreshButtonDelegate { + func buttonTapped() { + self.refresh() + } +} + // MARK: - Delegate: GetOffAlarmButton extension AlarmSettingViewController: GetOffAlarmButtonDelegate { func shouldGoToMovingStatusScene(from cell: UITableViewCell) { diff --git a/BBus/BBus/Foreground/AlarmSetting/UseCase/AlarmSettingAPIUsable.swift b/BBus/BBus/Foreground/AlarmSetting/UseCase/AlarmSettingAPIUsable.swift new file mode 100644 index 00000000..e917998a --- /dev/null +++ b/BBus/BBus/Foreground/AlarmSetting/UseCase/AlarmSettingAPIUsable.swift @@ -0,0 +1,14 @@ +// +// AlarmSettingAPIUsable.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation +import Combine + +protocol AlarmSettingAPIUsable: BaseUseCase { + func busArriveInfoWillLoaded(stId: String, busRouteId: String, ord: String) -> AnyPublisher + func busStationsInfoWillLoaded(busRouetId: String, arsId: String) -> AnyPublisher<[StationByRouteListDTO]?, Error> +} diff --git a/BBus/BBus/Foreground/AlarmSetting/UseCase/AlarmSettingAPIUseCase.swift b/BBus/BBus/Foreground/AlarmSetting/UseCase/AlarmSettingAPIUseCase.swift new file mode 100644 index 00000000..ba2e76a0 --- /dev/null +++ b/BBus/BBus/Foreground/AlarmSetting/UseCase/AlarmSettingAPIUseCase.swift @@ -0,0 +1,46 @@ +// +// AlarmSettingUseCase.swift +// BBus +// +// Created by 김태훈 on 2021/11/01. +// + +import Foundation +import Combine + +final class AlarmSettingAPIUseCase: AlarmSettingAPIUsable { + typealias AlarmSettingUseCases = GetArrInfoByRouteListUsable & GetStationsByRouteListUsable + + private let useCases: AlarmSettingUseCases + @Published private(set) var networkError: Error? + + init(useCases: AlarmSettingUseCases) { + self.useCases = useCases + self.networkError = nil + } + + func busArriveInfoWillLoaded(stId: String, busRouteId: String, ord: String) -> AnyPublisher { + return self.useCases.getArrInfoByRouteList(stId: stId, + busRouteId: busRouteId, + ord: ord) + .decode(type: ArrInfoByRouteResult.self, decoder: JSONDecoder()) + .tryMap({ item -> ArrInfoByRouteDTO in + let result = item.msgBody.itemList + guard let item = result.first else { throw BBusAPIError.wrongFormatError } + return item + }) + .eraseToAnyPublisher() + } + + func busStationsInfoWillLoaded(busRouetId: String, arsId: String) -> AnyPublisher<[StationByRouteListDTO]?, Error> { + return self.useCases.getStationsByRouteList(busRoutedId: busRouetId) + .decode(type: StationByRouteResult.self, decoder: JSONDecoder()) + .map({ item -> [StationByRouteListDTO]? in + let result = item.msgBody.itemList + guard let index = result.firstIndex(where: { $0.arsId == arsId }) else { return nil } + return Array(result[index.. - - init(useCases: AlarmSettingUseCases) { - self.useCases = useCases - self.busArriveInfo = nil - self.busStationsInfo = [] - self.networkError = nil - self.cancellables = [] - } - - func busArriveInfoWillLoaded(stId: String, busRouteId: String, ord: String) { - Self.queue.async { - self.useCases.getArrInfoByRouteList(stId: stId, - busRouteId: busRouteId, - ord: ord) - .receive(on: Self.queue) - .decode(type: ArrInfoByRouteResult.self, decoder: JSONDecoder()) - .tryMap({ item in - let result = item.msgBody.itemList - guard let item = result.first else { throw BBusAPIError.wrongFormatError } - return item - }) - .retry ({ [weak self] in - self?.busArriveInfoWillLoaded(stId: stId, busRouteId: busRouteId, ord: ord) - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$busArriveInfo) - } - } - - func busStationsInfoWillLoaded(busRouetId: String, arsId: String) { - Self.queue.async { - self.useCases.getStationsByRouteList(busRoutedId: busRouetId) - .receive(on: Self.queue) - .decode(type: StationByRouteResult.self, decoder: JSONDecoder()) - .retry({ [weak self] in - self?.busStationsInfoWillLoaded(busRouetId: busRouetId, arsId: arsId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .map({ item in - let result = item.msgBody.itemList - guard let index = result.firstIndex(where: { $0.arsId == arsId }) else { return nil } - return Array(result[index.. private var observer: NSObjectProtocol? - init(useCase: AlarmSettingUseCase, stationId: Int, busRouteId: Int, stationOrd: Int, arsId: String, routeType: RouteType?, busName: String) { - self.useCase = useCase + init(apiUseCase: AlarmSettingAPIUsable, calculateUseCase: AlarmSettingCalculatable, stationId: Int, busRouteId: Int, stationOrd: Int, arsId: String, routeType: RouteType?, busName: String) { + self.apiUseCase = apiUseCase + self.calculateUseCase = calculateUseCase self.stationId = stationId self.busRouteId = busRouteId self.stationOrd = stationOrd @@ -34,10 +37,9 @@ final class AlarmSettingViewModel { self.cancellables = [] self.busArriveInfos = AlarmSettingBusArriveInfos(arriveInfos: [], changedByTimer: false) self.busStationInfos = nil - self.errorMessage = nil - self.binding() - self.refresh() - self.showBusStations() + self.networkError = nil + self.loaderActiveStatus = true + self.bind() } func configureObserver() { @@ -54,25 +56,27 @@ final class AlarmSettingViewModel { } @objc func refresh() { - self.useCase.busArriveInfoWillLoaded(stId: "\(self.stationId)", - busRouteId: "\(self.busRouteId)", - ord: "\(self.stationOrd)") - } - - private func showBusStations() { - self.useCase.busStationsInfoWillLoaded(busRouetId: "\(self.busRouteId)", arsId: self.arsId) + self.bindBusArriveInfo() } - private func binding() { + private func bind() { self.bindBusArriveInfo() self.bindBusStationsInfo() + self.bindAlarmSettingViewModelInfo() } private func bindBusArriveInfo() { - self.useCase.$busArriveInfo - .receive(on: AlarmSettingUseCase.queue) - .sink(receiveValue: { [weak self] data in - guard let data = data else { return } + self.apiUseCase.busArriveInfoWillLoaded(stId: "\(self.stationId)", + busRouteId: "\(self.busRouteId)", + ord: "\(self.stationOrd)") + .first() + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + self?.networkError = error + self?.loaderActiveStatus = false + }) + .compactMap({$0}) + .map({ data in var arriveInfos: [AlarmSettingBusArriveInfo] = [] arriveInfos.append(AlarmSettingBusArriveInfo(busArriveRemainTime: data.firstBusArriveRemainTime, congestion: data.firstBusCongestion, @@ -85,52 +89,59 @@ final class AlarmSettingViewModel { currentStation: data.secondBusCurrentStation, plainNumber: data.secondBusPlainNumber, vehicleId: data.secondBusVehicleId)) - self?.busArriveInfos = AlarmSettingBusArriveInfos(arriveInfos: arriveInfos, changedByTimer: false) + return AlarmSettingBusArriveInfos(arriveInfos: arriveInfos, changedByTimer: false) }) - .store(in: &self.cancellables) + .assign(to: &self.$busArriveInfos) } private func bindBusStationsInfo() { - self.useCase.$busStationsInfo - .receive(on: AlarmSettingUseCase.queue) - .sink(receiveValue: { [weak self] infos in - self?.mapStationsDTOtoAlarmSettingInfo() - }) - .store(in: &self.cancellables) - } - - private func mapStationsDTOtoAlarmSettingInfo() { let initInfo: AlarmSettingBusStationInfo initInfo.estimatedTime = 0 initInfo.arsId = "" initInfo.name = "" initInfo.ord = 0 - - if let busStationsInfo = self.useCase.busStationsInfo { - busStationsInfo.publisher - .scan(initInfo, { before, info in - let alarmSettingInfo: AlarmSettingBusStationInfo - alarmSettingInfo.arsId = info.arsId - alarmSettingInfo.estimatedTime = before.estimatedTime + (before.arsId != "" ? MovingStatusViewModel.averageSectionTime(speed: info.sectionSpeed, distance: info.fullSectionDistance) : 0) - alarmSettingInfo.name = info.stationName - alarmSettingInfo.ord = info.sequence - return alarmSettingInfo - }) - .collect() - .map { $0 as [AlarmSettingBusStationInfo]? } - .assign(to: &self.$busStationInfos) - } - else { - self.busStationInfos = nil - } + + self.apiUseCase.busStationsInfoWillLoaded(busRouetId: "\(self.busRouteId)", arsId: self.arsId) + .first() + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + self?.networkError = error + self?.loaderActiveStatus = false + }) + .compactMap({ [weak self] result -> [StationByRouteListDTO]? in + if result == nil { self?.busStationInfos = nil } + return result + }) + .flatMap({ result in + return result.publisher + }) + .scan(initInfo, { before, info in + let alarmSettingInfo: AlarmSettingBusStationInfo + alarmSettingInfo.arsId = info.arsId + alarmSettingInfo.estimatedTime = before.estimatedTime + (before.arsId != "" ? self.calculateUseCase.averageSectionTime(speed: info.sectionSpeed, distance: info.fullSectionDistance) : 0) + alarmSettingInfo.name = info.stationName + alarmSettingInfo.ord = info.sequence + return alarmSettingInfo + }) + .collect() + .map { $0 as [AlarmSettingBusStationInfo]? } + .assign(to: &self.$busStationInfos) } - func sendErrorMessage(_ message: String) { - self.errorMessage = message + private func bindAlarmSettingViewModelInfo() { + self.$busArriveInfos.compactMap({$0}) + .combineLatest(self.$busStationInfos.dropFirst()) + .filter({ [weak self] aa in + guard let self = self else { return false } + return self.loaderActiveStatus + }) + .sink(receiveValue: { _ in + self.loaderActiveStatus = false + }) + .store(in: &self.cancellables) } - - func isStopLoader() -> Bool { - guard let busStationInfos = self.busStationInfos else { return false } - return !self.busArriveInfos.arriveInfos.isEmpty && !busStationInfos.isEmpty + + func activateLoaderActiveStatus() { + self.loaderActiveStatus = true } } diff --git a/BBus/BBus/Foreground/BusRoute/BusRouteCoordinator.swift b/BBus/BBus/Foreground/BusRoute/BusRouteCoordinator.swift index 9bf5608b..994cc8ea 100644 --- a/BBus/BBus/Foreground/BusRoute/BusRouteCoordinator.swift +++ b/BBus/BBus/Foreground/BusRoute/BusRouteCoordinator.swift @@ -16,8 +16,12 @@ final class BusRouteCoordinator: StationPushable { } func start(busRouteId: Int) { - let usecase = BusRouteUsecase(usecases: BBusAPIUsecases(on: BusRouteUsecase.queue)) - let viewModel = BusRouteViewModel(usecase: usecase, busRouteId: busRouteId) + let apiUseCases = BBusAPIUseCases(networkService: NetworkService(), + persistenceStorage: PersistenceStorage(), + tokenManageType: TokenManager.self, + requestFactory: RequestFactory()) + let useCase = BusRouteAPIUseCase(useCases: apiUseCases) + let viewModel = BusRouteViewModel(useCase: useCase, busRouteId: busRouteId) let viewController = BusRouteViewController(viewModel: viewModel) viewController.coordinator = self self.navigationPresenter.pushViewController(viewController, animated: true) diff --git a/BBus/BBus/Foreground/BusRoute/BusRouteViewController.swift b/BBus/BBus/Foreground/BusRoute/BusRouteViewController.swift index da08d05b..9e496b20 100644 --- a/BBus/BBus/Foreground/BusRoute/BusRouteViewController.swift +++ b/BBus/BBus/Foreground/BusRoute/BusRouteViewController.swift @@ -8,30 +8,13 @@ import UIKit import Combine -final class BusRouteViewController: UIViewController { - +final class BusRouteViewController: UIViewController, BaseViewControllerType { + weak var coordinator: BusRouteCoordinator? - private lazy var customNavigationBar = CustomNavigationBar() - private lazy var busRouteView = BusRouteView() private let viewModel: BusRouteViewModel? + private lazy var busRouteView = BusRouteView() + private var cancellables: Set = [] - private var busTags: [BusTagView] = [] - private var busIcon: UIImage? - - private lazy var refreshButton: UIButton = { - let radius: CGFloat = 25 - - let button = UIButton() - button.setImage(BBusImage.refresh, for: .normal) - button.layer.cornerRadius = radius - button.tintColor = BBusColor.white - button.backgroundColor = BBusColor.darkGray - - button.addAction(UIAction(handler: { [weak self] _ in - self?.viewModel?.refreshBusPos() - }), for: .touchUpInside) - return button - }() init(viewModel: BusRouteViewModel) { self.viewModel = viewModel @@ -45,32 +28,28 @@ final class BusRouteViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + self.baseViewDidLoad() self.busRouteView.startLoader() - self.binding() - self.configureLayout() - self.configureDelegate() self.configureBaseColor() - self.fetch() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + self.baseViewWillAppear() + self.viewModel?.configureObserver() - self.viewModel?.refreshBusPos() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + self.viewModel?.cancelObserver() } // MARK: - Configure - private func configureLayout() { - let refreshButtonWidthAnchor: CGFloat = 50 - let refreshTrailingBottomInterval: CGFloat = -16 - - self.view.addSubviews(self.busRouteView, self.customNavigationBar, self.refreshButton) + func configureLayout() { + self.view.addSubviews(self.busRouteView) NSLayoutConstraint.activate([ self.busRouteView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), @@ -79,81 +58,18 @@ final class BusRouteViewController: UIViewController { self.busRouteView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) ]) - NSLayoutConstraint.activate([ - self.customNavigationBar.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), - self.customNavigationBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), - self.customNavigationBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) - ]) - self.busRouteView.configureTableViewHeight(count: 20) - NSLayoutConstraint.activate([ - self.refreshButton.widthAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), - self.refreshButton.heightAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), - self.refreshButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: refreshTrailingBottomInterval), - self.refreshButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: refreshTrailingBottomInterval) - ]) } - private func configureDelegate() { + func configureDelegate() { self.busRouteView.configureDelegate(self) - self.customNavigationBar.configureDelegate(self) - } - - private func configureBaseColor() { - self.view.backgroundColor = BBusColor.gray - self.customNavigationBar.configureBackgroundColor(color: BBusColor.gray) - self.customNavigationBar.configureTintColor(color: BBusColor.white) - self.customNavigationBar.configureAlpha(alpha: 0) - self.busRouteView.configureColor(to: BBusColor.gray) } - private func configureBusColor(type: RouteType) { - let color: UIColor? - - switch type { - case .mainLine: - color = BBusColor.bbusTypeBlue - self.busIcon = BBusImage.blueBusIcon - case .broadArea: - color = BBusColor.bbusTypeRed - self.busIcon = BBusImage.redBusIcon - case .customized: - color = BBusColor.bbusTypeGreen - self.busIcon = BBusImage.greenBusIcon - case .circulation: - color = BBusColor.bbusTypeCirculation - self.busIcon = BBusImage.circulationBusIcon - case .lateNight: - color = BBusColor.bbusTypeBlue - self.busIcon = BBusImage.blueBusIcon - case .localLine: - color = BBusColor.bbusTypeGreen - self.busIcon = BBusImage.greenBusIcon - } - - self.view.backgroundColor = color - self.customNavigationBar.configureBackgroundColor(color: color) - self.customNavigationBar.configureTintColor(color: BBusColor.white) - self.customNavigationBar.configureAlpha(alpha: 0) - self.busRouteView.configureColor(to: color) - } - - private func configureBusTags(buses: [BusPosInfo]) { - self.busTags.forEach { $0.removeFromSuperview() } - self.busTags.removeAll() - - buses.forEach { [weak self] bus in - guard let self = self else { return } - let tag = self.busRouteView.createBusTag(location: bus.location, - busIcon: self.busIcon, - busNumber: bus.number, - busCongestion: bus.congestion.toString(), - isLowFloor: bus.islower) - self.busTags.append(tag) - } + func refresh() { + self.viewModel?.refreshBusPos() } - - private func binding() { + + func bindAll() { self.bindLoader() self.bindBusRouteHeaderResult() self.bindBusRouteBodyResult() @@ -161,21 +77,22 @@ final class BusRouteViewController: UIViewController { self.bindNetworkError() } + private func configureBaseColor() { + self.view.backgroundColor = BBusColor.gray + self.busRouteView.configureColor(to: BBusColor.gray) + } + private func bindBusRouteHeaderResult() { self.viewModel?.$header .receive(on: DispatchQueue.main) - .dropFirst() .sink(receiveValue: { [weak self] header in if let header = header { - self?.customNavigationBar.configureBackButtonTitle(header.busRouteName) + self?.busRouteView.configureBackButtonTitle(title: header.busRouteName) self?.busRouteView.configureHeaderView(busType: header.routeType.rawValue+"버스", busNumber: header.busRouteName, fromStation: header.startStation, toStation: header.endStation) - self?.configureBusColor(type: header.routeType) - } - else { - self?.noInfoAlert() + self?.view.backgroundColor = self?.busRouteView.configureBusColor(type: header.routeType) } }) .store(in: &self.cancellables) @@ -197,7 +114,7 @@ final class BusRouteViewController: UIViewController { .sink(receiveValue: { [weak self] buses in guard let viewModel = self?.viewModel else { return } - self?.configureBusTags(buses: buses) + self?.busRouteView.configureBusTags(buses: buses) if viewModel.stopLoader { self?.busRouteView.stopLoader() @@ -207,7 +124,7 @@ final class BusRouteViewController: UIViewController { } private func bindNetworkError() { - self.viewModel?.usecase.$networkError + self.viewModel?.$networkError .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] error in guard let _ = error else { return } @@ -244,10 +161,6 @@ final class BusRouteViewController: UIViewController { controller.addAction(action) self.coordinator?.delegate?.presentAlertToNavigation(controller: controller, completion: nil) } - - private func fetch() { - self.viewModel?.fetch() - } } // MARK: - DataSource : TableView @@ -288,10 +201,10 @@ extension BusRouteViewController: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { let baseLineContentOffset = BusRouteHeaderView.headerHeight - CustomNavigationBar.height if scrollView.contentOffset.y >= baseLineContentOffset { - self.customNavigationBar.configureAlpha(alpha: 1) + self.busRouteView.configureNavigationAlpha(alpha: 1) } else { - self.customNavigationBar.configureAlpha(alpha: CGFloat(scrollView.contentOffset.y/baseLineContentOffset)) + self.busRouteView.configureNavigationAlpha(alpha: CGFloat(scrollView.contentOffset.y/baseLineContentOffset)) } } @@ -319,3 +232,10 @@ extension BusRouteViewController: BackButtonDelegate { self.coordinator?.terminate() } } + +// MARK: - Delegate: RefreshButton +extension BusRouteViewController: RefreshButtonDelegate { + func buttonTapped() { + self.refresh() + } +} diff --git a/BBus/BBus/Foreground/BusRoute/UseCase/BusRouteAPIUsable.swift b/BBus/BBus/Foreground/BusRoute/UseCase/BusRouteAPIUsable.swift new file mode 100644 index 00000000..8d0138ec --- /dev/null +++ b/BBus/BBus/Foreground/BusRoute/UseCase/BusRouteAPIUsable.swift @@ -0,0 +1,15 @@ +// +// BusRouteAPIUsable.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation +import Combine + +protocol BusRouteAPIUsable: BaseUseCase { + func searchHeader(busRouteId: Int) -> AnyPublisher + func fetchRouteList(busRouteId: Int) -> AnyPublisher<[StationByRouteListDTO], Error> + func fetchBusPosList(busRouteId: Int) -> AnyPublisher<[BusPosByRtidDTO], Error> +} diff --git a/BBus/BBus/Foreground/BusRoute/UseCase/BusRouteAPIUseCase.swift b/BBus/BBus/Foreground/BusRoute/UseCase/BusRouteAPIUseCase.swift new file mode 100644 index 00000000..83cbd183 --- /dev/null +++ b/BBus/BBus/Foreground/BusRoute/UseCase/BusRouteAPIUseCase.swift @@ -0,0 +1,46 @@ +// +// BusRouteUseCase.swift +// BBus +// +// Created by 김태훈 on 2021/11/01. +// + +import Foundation +import Combine + +final class BusRouteAPIUseCase: BusRouteAPIUsable { + + private let useCases: GetRouteListUsable & GetStationsByRouteListUsable & GetBusPosByRtidUsable + + init(useCases: GetRouteListUsable & GetStationsByRouteListUsable & GetBusPosByRtidUsable) { + self.useCases = useCases + } + + func searchHeader(busRouteId: Int) -> AnyPublisher { + return self.useCases.getRouteList() + .decode(type: [BusRouteDTO].self, decoder: JSONDecoder()) + .tryMap({ routeList -> BusRouteDTO? in + let header = routeList.filter { $0.routeID == busRouteId }.first + return header + }) + .eraseToAnyPublisher() + } + + func fetchRouteList(busRouteId: Int) -> AnyPublisher<[StationByRouteListDTO], Error> { + return self.useCases.getStationsByRouteList(busRoutedId: "\(busRouteId)") + .decode(type: StationByRouteResult.self, decoder: JSONDecoder()) + .map({ item in + item.msgBody.itemList + }) + .eraseToAnyPublisher() + } + + func fetchBusPosList(busRouteId: Int) -> AnyPublisher<[BusPosByRtidDTO], Error> { + return self.useCases.getBusPosByRtid(busRoutedId: "\(busRouteId)") + .decode(type: BusPosByRtidResult.self, decoder: JSONDecoder()) + .tryMap({ item in + return item.msgBody.itemList + }) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Foreground/BusRoute/UseCase/BusRouteUseCase.swift b/BBus/BBus/Foreground/BusRoute/UseCase/BusRouteUseCase.swift deleted file mode 100644 index bc4574d6..00000000 --- a/BBus/BBus/Foreground/BusRoute/UseCase/BusRouteUseCase.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// BusRouteUseCase.swift -// BBus -// -// Created by 김태훈 on 2021/11/01. -// - -import Foundation -import Combine - -final class BusRouteUsecase { - - private let usecases: GetRouteListUsecase & GetStationsByRouteListUsecase & GetBusPosByRtidUsecase - @Published var header: BusRouteDTO? - @Published var bodys: [StationByRouteListDTO] = [] - @Published var buses: [BusPosByRtidDTO] = [] - @Published var networkError: Error? - private var cancellables: Set - static let queue = DispatchQueue(label: "BusRoute") - - init(usecases: GetRouteListUsecase & GetStationsByRouteListUsecase & GetBusPosByRtidUsecase) { - self.usecases = usecases - self.cancellables = [] - self.networkError = nil - } - - func searchHeader(busRouteId: Int) { - Self.queue.async { - self.usecases.getRouteList() - .receive(on: Self.queue) - .decode(type: [BusRouteDTO].self, decoder: JSONDecoder()) - .tryMap({ routeList -> BusRouteDTO? in - let header = routeList.filter { $0.routeID == busRouteId }.first - return header - }) - .retry({ [weak self] in - self?.searchHeader(busRouteId: busRouteId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$header) - } - } - - func fetchRouteList(busRouteId: Int) { - Self.queue.async { - self.usecases.getStationsByRouteList(busRoutedId: "\(busRouteId)") - .receive(on: Self.queue) - .decode(type: StationByRouteResult.self, decoder: JSONDecoder()) - .retry({ [weak self] in - self?.fetchRouteList(busRouteId: busRouteId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .map({ item in - item.msgBody.itemList - }) - .assign(to: &self.$bodys) - } - } - - func fetchBusPosList(busRouteId: Int) { - Self.queue.async { - self.usecases.getBusPosByRtid(busRoutedId: "\(busRouteId)") - .receive(on: Self.queue) - .decode(type: BusPosByRtidResult.self, decoder: JSONDecoder()) - .tryMap({ item in - return item.msgBody.itemList - }) - .retry({ [weak self] in - self?.fetchBusPosList(busRouteId: busRouteId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$buses) - } - } -} diff --git a/BBus/BBus/Foreground/BusRoute/View/BusRouteView.swift b/BBus/BBus/Foreground/BusRoute/View/BusRouteView.swift index e43baad6..17210698 100644 --- a/BBus/BBus/Foreground/BusRoute/View/BusRouteView.swift +++ b/BBus/BBus/Foreground/BusRoute/View/BusRouteView.swift @@ -7,7 +7,7 @@ import UIKit -final class BusRouteView: UIView { +final class BusRouteView: NavigatableView { private lazy var busRouteScrollView = UIScrollView() private lazy var busRouteScrollContentsView = UIView() @@ -29,8 +29,11 @@ final class BusRouteView: UIView { }() private lazy var loader: UIActivityIndicatorView = { let loader = UIActivityIndicatorView(style: .large) + loader.color = BBusColor.gray return loader }() + private var busTags: [BusTagView] = [] + private var busIcon: UIImage? private var busRouteTableViewHeightConstraint: NSLayoutConstraint? private var tableViewMinHeight: CGFloat { return max(self.frame.height - BusRouteHeaderView.headerHeight, 0) @@ -44,7 +47,7 @@ final class BusRouteView: UIView { } // MARK: - Configure - private func configureLayout() { + override func configureLayout() { let colorBackgroundViewHeightMultiplier: CGFloat = 0.5 self.addSubviews(self.colorBackgroundView, self.busRouteScrollView, self.loader) @@ -94,17 +97,24 @@ final class BusRouteView: UIView { self.loader.centerXAnchor.constraint(equalTo: self.centerXAnchor), self.loader.centerYAnchor.constraint(equalTo: self.centerYAnchor) ]) + + super.configureLayout() } - func configureDelegate(_ delegate: UITableViewDelegate & UITableViewDataSource & UIScrollViewDelegate) { + func configureDelegate(_ delegate: UITableViewDelegate & UITableViewDataSource & UIScrollViewDelegate & BackButtonDelegate & RefreshButtonDelegate) { self.busRouteTableView.delegate = delegate self.busRouteTableView.dataSource = delegate self.busRouteScrollView.delegate = delegate + self.refreshButton.configureDelegate(delegate) + self.navigationBar.configureDelegate(delegate) } func configureColor(to color: UIColor?) { self.colorBackgroundView.backgroundColor = color self.busHeaderView.backgroundColor = color + self.navigationBar.configureBackgroundColor(color: color) + self.navigationBar.configureTintColor(color: BBusColor.white) + self.navigationBar.configureAlpha(alpha: 0) } func configureTableViewHeight(count: Int) { @@ -153,4 +163,61 @@ final class BusRouteView: UIView { self.loader.isHidden = true self.loader.stopAnimating() } + + func configureBusColor(type: RouteType) -> UIColor? { + let color: UIColor? + + switch type { + case .mainLine: + color = BBusColor.bbusTypeBlue + self.busIcon = BBusImage.blueBusIcon + case .broadArea: + color = BBusColor.bbusTypeRed + self.busIcon = BBusImage.redBusIcon + case .customized: + color = BBusColor.bbusTypeGreen + self.busIcon = BBusImage.greenBusIcon + case .circulation: + color = BBusColor.bbusTypeCirculation + self.busIcon = BBusImage.circulationBusIcon + case .lateNight: + color = BBusColor.bbusTypeBlue + self.busIcon = BBusImage.blueBusIcon + case .localLine: + color = BBusColor.bbusTypeGreen + self.busIcon = BBusImage.greenBusIcon + case .town: + color = BBusColor.bbusTypeGreen + self.busIcon = BBusImage.greenBusIcon + case .airport: + color = BBusColor.bbusLikeYellow + self.busIcon = BBusImage.blueBusIcon + } + + self.configureColor(to: color) + return color + } + + func configureBusTags(buses: [BusPosInfo]) { + self.busTags.forEach { $0.removeFromSuperview() } + self.busTags.removeAll() + + buses.forEach { [weak self] bus in + guard let self = self else { return } + let tag = self.createBusTag(location: bus.location, + busIcon: self.busIcon, + busNumber: bus.number, + busCongestion: bus.congestion.toString(), + isLowFloor: bus.islower) + self.busTags.append(tag) + } + } + + func configureBackButtonTitle(title: String) { + self.navigationBar.configureBackButtonTitle(title) + } + + func configureNavigationAlpha(alpha: CGFloat) { + self.navigationBar.configureAlpha(alpha: alpha) + } } diff --git a/BBus/BBus/Foreground/BusRoute/ViewModel/BusRouteViewModel.swift b/BBus/BBus/Foreground/BusRoute/ViewModel/BusRouteViewModel.swift index 5479106a..86ef284a 100644 --- a/BBus/BBus/Foreground/BusRoute/ViewModel/BusRouteViewModel.swift +++ b/BBus/BBus/Foreground/BusRoute/ViewModel/BusRouteViewModel.swift @@ -14,18 +14,18 @@ typealias BusPosInfo = (location: CGFloat, number: String, congestion: BusConges final class BusRouteViewModel { - let usecase: BusRouteUsecase + let useCase: BusRouteAPIUsable private var cancellables: Set private let busRouteId: Int @Published var header: BusRouteDTO? @Published var bodys: [BusStationInfo] @Published var buses: [BusPosInfo] @Published private(set) var stopLoader: Bool = false + @Published private(set) var networkError: Error? - init(usecase: BusRouteUsecase, busRouteId: Int) { - self.usecase = usecase + init(useCase: BusRouteAPIUsable, busRouteId: Int) { + self.useCase = useCase self.busRouteId = busRouteId - self.header = nil self.cancellables = [] self.bodys = [] self.buses = [] @@ -44,26 +44,33 @@ final class BusRouteViewModel { } private func bindHeaderInfo() { - self.usecase.$header - .receive(on: BusRouteUsecase.queue) + self.useCase.searchHeader(busRouteId: self.busRouteId) + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + self?.networkError = error + }) .assign(to: &self.$header) } private func bindBodysInfo() { - self.usecase.$bodys - .receive(on: BusRouteUsecase.queue) + self.useCase.fetchRouteList(busRouteId: self.busRouteId) + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + self?.networkError = error + }) .sink(receiveValue: { [weak self] bodys in - guard let self = self else { return } - - self.convertBusStationInfo(with: bodys) - self.usecase.fetchBusPosList(busRouteId: self.busRouteId) + self?.convertBusStationInfo(with: bodys) }) .store(in: &self.cancellables) } private func bindBusesPosInfo() { - self.usecase.$buses - .receive(on: BusRouteUsecase.queue) + self.useCase.fetchBusPosList(busRouteId: self.busRouteId) + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + guard error as? BBusAPIError != BBusAPIError.noneResultError else { return } + self?.networkError = error + }) .sink(receiveValue: { [weak self] buses in self?.convertBusPosInfo(with: buses) }) @@ -117,13 +124,8 @@ final class BusRouteViewModel { self.buses = busesResult } - func fetch() { - self.usecase.searchHeader(busRouteId: self.busRouteId) - self.usecase.fetchRouteList(busRouteId: self.busRouteId) - } - @objc func refreshBusPos() { - self.usecase.fetchBusPosList(busRouteId: self.busRouteId) + self.bindBusesPosInfo() } func isStopLoader() -> Bool { @@ -132,8 +134,8 @@ final class BusRouteViewModel { private func bindLoader() { self.$header.zip(self.$bodys) - .receive(on: BusRouteUsecase.queue) - .output(at: 2) + .receive(on: DispatchQueue.global()) + .dropFirst() .sink(receiveValue: { result in self.stopLoader = true }) diff --git a/BBus/BBus/Foreground/Home/HomeCoordinator.swift b/BBus/BBus/Foreground/Home/HomeCoordinator.swift index d8e7206c..e30af905 100644 --- a/BBus/BBus/Foreground/Home/HomeCoordinator.swift +++ b/BBus/BBus/Foreground/Home/HomeCoordinator.swift @@ -15,11 +15,17 @@ final class HomeCoordinator: SearchPushable, BusRoutePushable, AlarmSettingPusha self.navigationPresenter = presenter } - func start() { - let useCase = HomeUseCase(usecases: BBusAPIUsecases(on: HomeUseCase.queue)) - let viewModel = HomeViewModel(useCase: useCase) - let viewController = HomeViewController(viewModel: viewModel) + func start(statusBarHeight: CGFloat?) { + let apiUseCases = BBusAPIUseCases(networkService: NetworkService(), + persistenceStorage: PersistenceStorage(), + tokenManageType: TokenManager.self, + requestFactory: RequestFactory()) + let apiUseCase = HomeAPIUseCase(useCases: apiUseCases) + let calculateUseCase = HomeCalculateUseCase() + let viewModel = HomeViewModel(apiUseCase: apiUseCase, calculateUseCase: calculateUseCase) + let viewController = HomeViewController(viewModel: viewModel, statusBarHegiht: statusBarHeight) viewController.coordinator = self + navigationPresenter.pushViewController(viewController, animated: false) // present } } diff --git a/BBus/BBus/Foreground/Home/HomeViewController.swift b/BBus/BBus/Foreground/Home/HomeViewController.swift index 4d22aa27..73cb0358 100644 --- a/BBus/BBus/Foreground/Home/HomeViewController.swift +++ b/BBus/BBus/Foreground/Home/HomeViewController.swift @@ -8,108 +8,95 @@ import UIKit import Combine -final class HomeViewController: UIViewController { - - private var lastContentOffset: CGFloat = 0 - private let refreshButtonWidth: CGFloat = 50 +final class HomeViewController: UIViewController, BaseViewControllerType { weak var coordinator: HomeCoordinator? private let viewModel: HomeViewModel? - private lazy var homeView = HomeView() - lazy var refreshButton: ThrottleButton = { - let button = ThrottleButton() - button.setImage(BBusImage.refresh, for: .normal) - button.layer.cornerRadius = self.refreshButtonWidth / 2 - button.tintColor = BBusColor.white - button.addTouchUpEventWithThrottle(delay: ThrottleButton.refreshInterval) { [weak self] in - self?.viewModel?.reloadFavoriteData() - } - return button - }() + private let statusBarHeight: CGFloat? + private var lastContentOffset: CGFloat = 0 private var cancellables: Set = [] - init(viewModel: HomeViewModel) { + init(viewModel: HomeViewModel, statusBarHegiht: CGFloat?) { self.viewModel = viewModel + self.statusBarHeight = statusBarHegiht super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { self.viewModel = nil + self.statusBarHeight = nil super.init(coder: coder) } override func viewDidLoad() { super.viewDidLoad() - self.title = "Home" + self.baseViewDidLoad() + + self.configureStatusBarLayout() self.configureColor() - self.configureLayout() - self.binding() - self.homeView.configureLayout() - self.homeView.configureDelegate(self) - - let app = UIApplication.shared - let statusBarHeight: CGFloat = app.statusBarFrame.size.height - - let statusbarView = UIView() - statusbarView.backgroundColor = BBusColor.white //컬러 설정 부분 - - self.view.addSubviews(statusbarView) - statusbarView.heightAnchor - .constraint(equalToConstant: statusBarHeight).isActive = true - statusbarView.widthAnchor - .constraint(equalTo: self.view.widthAnchor, multiplier: 1.0).isActive = true - statusbarView.topAnchor - .constraint(equalTo: self.view.topAnchor).isActive = true - statusbarView.centerXAnchor - .constraint(equalTo: self.view.centerXAnchor).isActive = true - } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - self.viewModel?.reloadFavoriteData() + self.baseViewWillAppear() + self.viewModel?.configureObserver() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + self.viewModel?.cancelObserver() } // MARK: - Configuration - private func configureLayout() { - self.view.addSubviews(self.homeView, self.refreshButton) - + func configureLayout() { + self.view.addSubviews(self.homeView) NSLayoutConstraint.activate([ self.homeView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), self.homeView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), self.homeView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor), self.homeView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor) ]) + + self.homeView.configureLayout() + } - self.refreshButton.backgroundColor = BBusColor.darkGray - let refreshTrailingBottomInterval: CGFloat = -16 + private func configureStatusBarLayout() { + guard let statusBarHeight = self.statusBarHeight else { return } + + let statusbarView = UIView() + statusbarView.backgroundColor = BBusColor.white //컬러 설정 부분 + + self.view.addSubviews(statusbarView) NSLayoutConstraint.activate([ - self.refreshButton.widthAnchor.constraint(equalToConstant: self.refreshButtonWidth), - self.refreshButton.heightAnchor.constraint(equalTo: self.refreshButton.widthAnchor), - self.refreshButton.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: refreshTrailingBottomInterval), - self.refreshButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: refreshTrailingBottomInterval) + statusbarView.heightAnchor.constraint(equalToConstant: statusBarHeight), + statusbarView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1.0), + statusbarView.topAnchor.constraint(equalTo: self.view.topAnchor), + statusbarView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor) ]) } - private func configureColor() { - self.view.backgroundColor = BBusColor.white + func configureDelegate() { + self.homeView.configureDelegate(self) } - - private func binding() { + + func refresh() { + self.viewModel?.loadHomeData() + } + + func bindAll() { self.bindFavoriteList() self.bindNetworkError() } + + private func configureColor() { + self.view.backgroundColor = BBusColor.white + } private func bindFavoriteList() { - self.viewModel?.$homeFavoriteList .compactMap { $0 } .filter { !$0.changedByTimer } @@ -123,7 +110,7 @@ final class HomeViewController: UIViewController { } private func bindNetworkError() { - self.viewModel?.useCase.$networkError + self.viewModel?.$networkError .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] error in guard let _ = error else { return } @@ -324,3 +311,10 @@ extension HomeViewController: FavoriteHeaderViewDelegate { self.coordinator?.pushToStation(arsId: arsId) } } + +// MARK: - RefreshButtonDelegate: HomeView +extension HomeViewController: RefreshButtonDelegate { + func buttonTapped() { + self.viewModel?.loadHomeData() + } +} diff --git a/BBus/BBus/Foreground/Home/Model/HomeModel.swift b/BBus/BBus/Foreground/Home/Model/HomeModel.swift index 911d4bf4..b6d1a02a 100644 --- a/BBus/BBus/Foreground/Home/Model/HomeModel.swift +++ b/BBus/BBus/Foreground/Home/Model/HomeModel.swift @@ -33,6 +33,20 @@ struct HomeFavoriteList { self.changedByTimer = false } + init(favorites: [HomeFavorite]) { + var orderedFavorites = [HomeFavorite]() + favorites.forEach { favorite in + if let index = orderedFavorites.firstIndex(where: { $0.stationId == favorite.stationId }) { + orderedFavorites[index].buses.append(contentsOf: favorite.buses) + } + else { + orderedFavorites.append(favorite) + } + } + self.favorites = orderedFavorites + self.changedByTimer = false + } + func count() -> Int { return self.favorites.count } @@ -61,11 +75,11 @@ struct HomeFavoriteList { } } -struct HomeFavorite: Equatable { +typealias HomeFavoriteInfo = (favoriteItem: FavoriteItemDTO, arriveInfo: HomeArriveInfo?) - typealias HomeBusInfo = (favoriteItem: FavoriteItemDTO, arriveInfo: HomeArriveInfo?) +struct HomeFavorite: Equatable { - subscript(index: Int) -> HomeBusInfo? { + subscript(index: Int) -> HomeFavoriteInfo? { guard 0.. AnyPublisher<[FavoriteItemDTO], Error> + func fetchBusRemainTime(favoriteItem: FavoriteItemDTO) -> AnyPublisher + func fetchStation() -> AnyPublisher<[StationDTO], Error> + func fetchBusRoute() -> AnyPublisher<[BusRouteDTO], Error> +} diff --git a/BBus/BBus/Foreground/Home/UseCase/HomeAPIUseCase.swift b/BBus/BBus/Foreground/Home/UseCase/HomeAPIUseCase.swift new file mode 100644 index 00000000..f356856a --- /dev/null +++ b/BBus/BBus/Foreground/Home/UseCase/HomeAPIUseCase.swift @@ -0,0 +1,64 @@ +// +// HomeUseCase.swift +// BBus +// +// Created by 김태훈 on 2021/11/01. +// + +import Foundation +import Combine + +final class HomeAPIUseCase: HomeAPIUsable { + + private let useCases: HomeUseCases + + init(useCases: HomeUseCases) { + self.useCases = useCases + } + + func fetchFavoriteData() -> AnyPublisher<[FavoriteItemDTO], Error> { + return self.useCases.getFavoriteItemList() + .decode(type: [FavoriteItemDTO]?.self, decoder: PropertyListDecoder()) + .tryMap({ item in + guard let item = item else { throw BBusAPIError.wrongFormatError } + return item + }) + .eraseToAnyPublisher() + } + + func fetchBusRemainTime(favoriteItem: FavoriteItemDTO) -> AnyPublisher { + return self.useCases.getArrInfoByRouteList(stId: favoriteItem.stId, + busRouteId: favoriteItem.busRouteId, + ord: favoriteItem.ord) + .decode(type: ArrInfoByRouteResult.self, decoder: JSONDecoder()) + .tryMap({ item in + let result = item.msgBody.itemList + guard let item = result.first else { throw BBusAPIError.wrongFormatError } + let homeFavoriteInfo: HomeFavoriteInfo + homeFavoriteInfo.favoriteItem = favoriteItem + homeFavoriteInfo.arriveInfo = HomeArriveInfo(arrInfoByRouteDTO: item) + return homeFavoriteInfo + }) + .eraseToAnyPublisher() + } + + func fetchStation() -> AnyPublisher<[StationDTO], Error> { + self.useCases.getStationList() + .decode(type: [StationDTO]?.self, decoder: JSONDecoder()) + .tryMap({ item in + guard let item = item else { throw BBusAPIError.wrongFormatError } + return item + }) + .eraseToAnyPublisher() + } + + func fetchBusRoute() -> AnyPublisher<[BusRouteDTO], Error> { + return self.useCases.getRouteList() + .decode(type: [BusRouteDTO]?.self, decoder: JSONDecoder()) + .tryMap({ item in + guard let item = item else { throw BBusAPIError.wrongFormatError } + return item + }) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Foreground/Home/UseCase/HomeCalculatable.swift b/BBus/BBus/Foreground/Home/UseCase/HomeCalculatable.swift new file mode 100644 index 00000000..bbc8efc6 --- /dev/null +++ b/BBus/BBus/Foreground/Home/UseCase/HomeCalculatable.swift @@ -0,0 +1,14 @@ +// +// HomeCalculatable.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation + +protocol HomeCalculateUsable: BaseUseCase { + func findStationName(in list: [StationDTO]?, by stationId: String) -> String? + func findBusName(in list: [BusRouteDTO]?, by busRouteId: String) -> String? + func findBusType(in list: [BusRouteDTO]?, by busName: String) -> RouteType? +} diff --git a/BBus/BBus/Foreground/Home/UseCase/HomeCalculateUseCase.swift b/BBus/BBus/Foreground/Home/UseCase/HomeCalculateUseCase.swift new file mode 100644 index 00000000..2c92bf6e --- /dev/null +++ b/BBus/BBus/Foreground/Home/UseCase/HomeCalculateUseCase.swift @@ -0,0 +1,28 @@ +// +// HomeCalculateUseCase.swift +// BBus +// +// Created by 김태훈 on 2021/11/29. +// + +import Foundation + +struct HomeCalculateUseCase: HomeCalculateUsable { + func findStationName(in list: [StationDTO]?, by stationId: String) -> String? { + guard let stationId = Int(stationId), + let stationName = list?.first(where: { $0.stationID == stationId })?.stationName else { return nil } + + return stationName + } + + func findBusName(in list: [BusRouteDTO]?, by busRouteId: String) -> String? { + guard let busRouteId = Int(busRouteId), + let busName = list?.first(where: { $0.routeID == busRouteId })?.busRouteName else { return nil } + + return busName + } + + func findBusType(in list: [BusRouteDTO]?, by busName: String) -> RouteType? { + return list?.first(where: { $0.busRouteName == busName } )?.routeType + } +} diff --git a/BBus/BBus/Foreground/Home/UseCase/HomeUseCase.swift b/BBus/BBus/Foreground/Home/UseCase/HomeUseCase.swift deleted file mode 100644 index 5b65c1d5..00000000 --- a/BBus/BBus/Foreground/Home/UseCase/HomeUseCase.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// HomeUseCase.swift -// BBus -// -// Created by 김태훈 on 2021/11/01. -// - -import Foundation -import Combine - -typealias HomeUseCases = GetFavoriteItemListUsecase & CreateFavoriteItemUsecase & GetStationListUsecase & GetRouteListUsecase & GetArrInfoByRouteListUsecase - -final class HomeUseCase { - - private let usecases: HomeUseCases - private var cancellables: Set - static let queue = DispatchQueue.init(label: "Home") - private(set) var stationList: [StationDTO]? - private(set) var busRouteList: [BusRouteDTO]? - @Published var favoriteList: [FavoriteItemDTO]? - @Published private(set) var networkError: Error? - - init(usecases: HomeUseCases) { - self.usecases = usecases - self.cancellables = [] - self.networkError = nil - self.startHome() - } - - private func startHome() { - self.loadFavoriteData() - self.loadStation() - self.loadRoute() - } - - func loadFavoriteData() { - Self.queue.async { - self.usecases.getFavoriteItemList() - .receive(on: Self.queue) - .decode(type: [FavoriteItemDTO]?.self, decoder: PropertyListDecoder()) - .retry({ [weak self] in - self?.loadFavoriteData() - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$favoriteList) - } - } - - func loadBusRemainTime(favoriteItem: FavoriteItemDTO, completion: @escaping (ArrInfoByRouteDTO) -> Void) { - Self.queue.async { - self.usecases.getArrInfoByRouteList(stId: favoriteItem.stId, - busRouteId: favoriteItem.busRouteId, - ord: favoriteItem.ord) - .receive(on: Self.queue) - .decode(type: ArrInfoByRouteResult.self, decoder: JSONDecoder()) - .tryMap({ item in - let result = item.msgBody.itemList - guard let item = result.first else { throw BBusAPIError.wrongFormatError } - return item - }) - .retry({ [weak self] in - self?.loadBusRemainTime(favoriteItem: favoriteItem, completion: completion) - }, handler: { [weak self] error in - self?.networkError = error - }) - .sink(receiveValue: { item in - completion(item) - }) - .store(in: &self.cancellables) - } - } - - private func loadStation() { - Self.queue.async { - self.usecases.getStationList() - .receive(on: Self.queue) - .decode(type: [StationDTO]?.self, decoder: JSONDecoder()) - .retry({ [weak self] in - self?.loadStation() - }, handler: { [weak self] error in - self?.networkError = error - }) - .sink(receiveValue: { [weak self] stationList in - self?.stationList = stationList - }) - .store(in: &self.cancellables) - } - } - - private func loadRoute() { - Self.queue.async { - self.usecases.getRouteList() - .receive(on: Self.queue) - .decode(type: [BusRouteDTO]?.self, decoder: JSONDecoder()) - .retry({ [weak self] in - self?.loadRoute() - }, handler: { [weak self] error in - self?.networkError = error - }) - .sink(receiveValue: { [weak self] busRouteList in - self?.busRouteList = busRouteList - }) - .store(in: &self.cancellables) - } - } -} diff --git a/BBus/BBus/Foreground/Home/View/FavoriteCollectionViewCell.swift b/BBus/BBus/Foreground/Home/View/FavoriteCollectionViewCell.swift index c844ae96..5411f05d 100644 --- a/BBus/BBus/Foreground/Home/View/FavoriteCollectionViewCell.swift +++ b/BBus/BBus/Foreground/Home/View/FavoriteCollectionViewCell.swift @@ -24,6 +24,7 @@ class FavoriteCollectionViewCell: UICollectionViewCell { } private lazy var loader: UIActivityIndicatorView = { let loader = UIActivityIndicatorView(style: .large) + loader.color = BBusColor.gray return loader }() class var height: CGFloat { return 70 } @@ -127,6 +128,10 @@ class FavoriteCollectionViewCell: UICollectionViewCell { self.busNumberLabel.textColor = BBusColor.bbusTypeBlue case .localLine: self.busNumberLabel.textColor = BBusColor.bbusTypeGreen + case .town: + self.busNumberLabel.textColor = BBusColor.bbusTypeGreen + case .airport: + self.busNumberLabel.textColor = BBusColor.bbusLikeYellow default: self.busNumberLabel.textColor = BBusColor.bbusGray } diff --git a/BBus/BBus/Foreground/Home/View/HomeView.swift b/BBus/BBus/Foreground/Home/View/HomeView.swift index 5373c21c..bcdb9d17 100644 --- a/BBus/BBus/Foreground/Home/View/HomeView.swift +++ b/BBus/BBus/Foreground/Home/View/HomeView.swift @@ -7,7 +7,7 @@ import UIKit -final class HomeView: UIView { +final class HomeView: RefreshableView { private lazy var favoriteCollectionView: UICollectionView = { let collectionView = UICollectionView(frame: CGRect(), collectionViewLayout: self.collectionViewLayout()) @@ -35,7 +35,8 @@ final class HomeView: UIView { } // MARK: - Configuration - func configureLayout() { + override func configureLayout() { + self.addSubviews(self.favoriteCollectionView, self.emptyFavoriteNotice, self.navigationView) self.favoriteCollectionView.contentInsetAdjustmentBehavior = .never @@ -60,12 +61,15 @@ final class HomeView: UIView { self.navigationView.leadingAnchor.constraint(equalTo: self.leadingAnchor), self.navigationView.trailingAnchor.constraint(equalTo: self.trailingAnchor) ]) + + super.configureLayout() } - func configureDelegate(_ delegate: UICollectionViewDelegate & UICollectionViewDataSource & UICollectionViewDelegateFlowLayout & HomeSearchButtonDelegate) { + func configureDelegate(_ delegate: UICollectionViewDelegate & UICollectionViewDataSource & UICollectionViewDelegateFlowLayout & HomeSearchButtonDelegate & RefreshButtonDelegate) { self.favoriteCollectionView.delegate = delegate self.favoriteCollectionView.dataSource = delegate self.navigationView.configureDelegate(delegate) + self.refreshButton.configureDelegate(delegate) } func configureNavigationViewVisable(_ direction: Bool) { @@ -91,6 +95,10 @@ final class HomeView: UIView { return section } + + func emptyNoticeActivate(by onOff: Bool) { + self.emptyFavoriteNotice.isHidden = !onOff + } private func collectionViewLayout() -> UICollectionViewLayout { let layout = UICollectionViewFlowLayout() @@ -103,8 +111,4 @@ final class HomeView: UIView { layout.minimumLineSpacing = bottomLineHeight return layout } - - func emptyNoticeActivate(by onOff: Bool) { - self.emptyFavoriteNotice.isHidden = !onOff - } } diff --git a/BBus/BBus/Foreground/Home/ViewModel/HomeViewModel.swift b/BBus/BBus/Foreground/Home/ViewModel/HomeViewModel.swift index 7374b312..fc64b42f 100644 --- a/BBus/BBus/Foreground/Home/ViewModel/HomeViewModel.swift +++ b/BBus/BBus/Foreground/Home/ViewModel/HomeViewModel.swift @@ -10,18 +10,29 @@ import Combine final class HomeViewModel { - let useCase: HomeUseCase - private var cancellable: AnyCancellable? + let apiUseCase: HomeAPIUsable + let calculateUseCase: HomeCalculateUsable + private var cancellables: Set @Published private(set) var homeFavoriteList: HomeFavoriteList? + private(set) var stationList: [StationDTO]? + private(set) var busRouteList: [BusRouteDTO]? - init(useCase: HomeUseCase) { - self.useCase = useCase - self.bindFavoriteData() + @Published private(set) var networkError: Error? + + init(apiUseCase: HomeAPIUsable, calculateUseCase: HomeCalculateUsable) { + self.apiUseCase = apiUseCase + self.calculateUseCase = calculateUseCase + self.cancellables = [] + self.loadBusRouteList() + self.loadStationList() + self.networkError = nil + + self.loadHomeData() } func configureObserver() { NotificationCenter.default.addObserver(self, selector: #selector(descendTime), name: .oneSecondPassed, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(reloadFavoriteData), name: .thirtySecondPassed, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(loadHomeData), name: .thirtySecondPassed, object: nil) } func cancelObserver() { @@ -32,41 +43,82 @@ final class HomeViewModel { self.homeFavoriteList?.descendAllTime() } - private func bindFavoriteData() { - self.cancellable = self.useCase.$favoriteList - .receive(on: HomeUseCase.queue) - .sink(receiveValue: { [weak self] favoriteItems in - guard let favoriteItems = favoriteItems else { return } + @objc func loadHomeData() { + self.apiUseCase.fetchFavoriteData() + .catchError({ [weak self] error in + self?.networkError = error + }) + .sink { [weak self] favoriteItems in self?.homeFavoriteList = HomeFavoriteList(dtoList: favoriteItems) - favoriteItems.forEach({ [weak self] favoriteItem in - self?.useCase.loadBusRemainTime(favoriteItem: favoriteItem) { arrInfoByRouteDTO in - guard let indexPath = self?.homeFavoriteList?.indexPath(of: favoriteItem) else { return } - let homeArrivalInfo = HomeArriveInfo(arrInfoByRouteDTO: arrInfoByRouteDTO) - self?.homeFavoriteList?.configure(homeArrivalinfo: homeArrivalInfo, indexPath: indexPath) - } - }) + self?.loadRemainTime(with: favoriteItems) + } + .store(in: &self.cancellables) + } + + private func loadRemainTime(with favoriteItems: [FavoriteItemDTO]) { + guard let homeFavoriteList = homeFavoriteList else { return } + + var newHomeFavoriteList = homeFavoriteList + favoriteItems.publisher + .receive(on: DispatchQueue.global()) + .flatMap({ [weak self] (favoriteItem) -> AnyPublisher in + guard let self = self else { return NetworkError.unknownError.publisher } + return self.apiUseCase.fetchBusRemainTime(favoriteItem: favoriteItem) + }) + .catchError({ [weak self] error in + self?.networkError = error }) + .map({ (homeFavoriteInfo) -> HomeFavoriteList? in + guard let indexPath = newHomeFavoriteList.indexPath(of: homeFavoriteInfo.favoriteItem), + let arriveInfo = homeFavoriteInfo.arriveInfo else { return nil } + newHomeFavoriteList.configure(homeArrivalinfo: arriveInfo, indexPath: indexPath) + return newHomeFavoriteList + }) + .compactMap({ $0 }) + .last() + .assign(to: &self.$homeFavoriteList) } - @objc func reloadFavoriteData() { - self.useCase.loadFavoriteData() + private func loadBusRouteList() { + self.apiUseCase.fetchBusRoute() + .catchError({ [weak self] error in + self?.networkError = error + }) + .sink { [weak self] busRouteDTOs in + self?.busRouteList = busRouteDTOs + } + .store(in: &self.cancellables) } - func stationName(by stationId: String) -> String? { - guard let stationId = Int(stationId), - let stationName = self.useCase.stationList?.first(where: { $0.stationID == stationId })?.stationName else { return nil } + private func loadStationList() { + self.apiUseCase.fetchStation() + .catchError({ [weak self] error in + self?.networkError = error + }) + .sink { [weak self] stationDTOs in + self?.stationList = stationDTOs + } + .store(in: &self.cancellables) + } - return stationName + func stationName(by stationId: String) -> String? { + return self.calculateUseCase.findStationName(in: self.stationList, by: stationId) } func busName(by busRouteId: String) -> String? { - guard let busRouteId = Int(busRouteId), - let busName = self.useCase.busRouteList?.first(where: { $0.routeID == busRouteId })?.busRouteName else { return nil } - - return busName + return self.calculateUseCase.findBusName(in: self.busRouteList, by: busRouteId) } func busType(by busName: String) -> RouteType? { - return self.useCase.busRouteList?.first(where: { $0.busRouteName == busName } )?.routeType + return self.calculateUseCase.findBusType(in: self.busRouteList, by: busName) + } +} + +fileprivate extension Error { + var publisher: AnyPublisher { + let publisher = CurrentValueSubject(nil) + publisher.send(completion: .failure(self)) + return publisher.compactMap({$0}) + .eraseToAnyPublisher() } } diff --git a/BBus/BBus/Foreground/MovingStatus/MovingStatusViewController.swift b/BBus/BBus/Foreground/MovingStatus/MovingStatusViewController.swift index 2c2608ad..709c14f8 100644 --- a/BBus/BBus/Foreground/MovingStatus/MovingStatusViewController.swift +++ b/BBus/BBus/Foreground/MovingStatus/MovingStatusViewController.swift @@ -11,33 +11,15 @@ import CoreLocation typealias MovingStatusCoordinator = MovingStatusOpenCloseDelegate & MovingStatusFoldUnfoldDelegate & AlertCreateToNavigationDelegate & AlertCreateToMovingStatusDelegate -final class MovingStatusViewController: UIViewController { - +final class MovingStatusViewController: UIViewController, BaseViewControllerType { + static private let alarmIdentifier: String = "GetOffAlarm" weak var coordinator: MovingStatusCoordinator? - private lazy var movingStatusView = MovingStatusView() private let viewModel: MovingStatusViewModel? + private lazy var movingStatusView = MovingStatusView() + private var cancellables: Set = [] - private var busTag: MovingStatusBusTagView? - private var color: UIColor? - private var busIcon: UIImage? - private var locationManager: CLLocationManager? - - private lazy var refreshButton: ThrottleButton = { - let radius: CGFloat = 25 - - let button = ThrottleButton() - button.setImage(BBusImage.refresh, for: .normal) - button.layer.cornerRadius = radius - button.tintColor = BBusColor.white - button.backgroundColor = BBusColor.darkGray - - button.addTouchUpEventWithThrottle(delay: ThrottleButton.refreshInterval) { [weak self] in - self?.viewModel?.updateAPI() - } - return button - }() init(viewModel: MovingStatusViewModel) { self.viewModel = viewModel @@ -51,48 +33,20 @@ final class MovingStatusViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + self.baseViewDidLoad() self.movingStatusView.startLoader() - self.binding() - self.configureLayout() - self.configureDelegate() - self.configureBusTag() - self.fetch() + self.movingStatusView.configureBusTag() self.configureLocationManager() - self.sendRequestAuthorization() - } - - private func sendRequestAuthorization() { - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .sound, .badge], completionHandler: { didAllow, error in - if let error = error { - print(error) - } - }) } private func configureLocationManager() { - // locationManager 인스턴스를 생성 - self.locationManager = CLLocationManager() - - // 앱을 사용할 때만 위치 정보를 허용할 경우 호출 - self.locationManager?.requestWhenInUseAuthorization() - - // 위치 정보 제공의 정확도를 설정할 수 있다. - self.locationManager?.desiredAccuracy = kCLLocationAccuracyBest - - // 백그라운드에서 위치 업데이트 - self.locationManager?.allowsBackgroundLocationUpdates = true - - // 위치 정보를 지속적으로 받고 싶은 경우 이벤트를 시작 - self.locationManager?.startUpdatingLocation() - - self.locationManager?.delegate = self + GetOffAlarmController.shared.configureAlarmPermission(self) } // MARK: - Configure - private func configureLayout() { - self.view.addSubviews(self.movingStatusView, self.refreshButton) + func configureLayout() { + self.view.addSubviews(self.movingStatusView) NSLayoutConstraint.activate([ self.movingStatusView.topAnchor.constraint(equalTo: self.view.topAnchor), @@ -100,44 +54,13 @@ final class MovingStatusViewController: UIViewController { self.movingStatusView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), self.movingStatusView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) ]) - - let refreshButtonWidthAnchor: CGFloat = 50 - let refreshBottomInterval: CGFloat = -MovingStatusView.endAlarmViewHeight - let refreshTrailingInterval: CGFloat = -16 - - NSLayoutConstraint.activate([ - self.refreshButton.widthAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), - self.refreshButton.heightAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), - self.refreshButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: refreshTrailingInterval), - self.refreshButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: refreshBottomInterval) - ]) } - private func configureDelegate() { + func configureDelegate() { self.movingStatusView.configureDelegate(self) } - - private func configureColor() { - self.view.backgroundColor = BBusColor.white - } - - private func configureBusTag(bus: BoardedBus? = nil) { - self.busTag?.removeFromSuperview() - if let bus = bus { - self.busTag = self.movingStatusView.createBusTag(location: bus.location, - color: self.color, - busIcon: self.busIcon, - remainStation: bus.remainStation) - } - else { - self.busTag = self.movingStatusView.createBusTag(color: self.color, - busIcon: self.busIcon, - remainStation: nil) - } - } - - private func binding() { + func bindAll() { self.bindLoader() self.bindHeaderBusInfo() self.bindRemainTime() @@ -164,8 +87,8 @@ final class MovingStatusViewController: UIViewController { .sink(receiveValue: { [weak self] busInfo in guard let busInfo = busInfo else { return } self?.movingStatusView.configureBusName(to: busInfo.busName) - self?.configureBusColor(type: busInfo.type) - self?.configureBusTag(bus: nil) + self?.movingStatusView.configureColorAndBusIcon(type: busInfo.type) + self?.movingStatusView.configureBusTag(bus: nil) }) .store(in: &self.cancellables) } @@ -201,7 +124,7 @@ final class MovingStatusViewController: UIViewController { self.viewModel?.$boardedBus .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] boardedBus in - self?.configureBusTag(bus: boardedBus) + self?.movingStatusView.configureBusTag(bus: boardedBus) }) .store(in: &self.cancellables) } @@ -227,38 +150,9 @@ final class MovingStatusViewController: UIViewController { }) .store(in: &self.cancellables) } - - private func configureBusColor(type: RouteType) { - switch type { - case .mainLine: - self.color = BBusColor.bbusTypeBlue - self.busIcon = BBusImage.blueBooduckBus - case .broadArea: - self.color = BBusColor.bbusTypeRed - self.busIcon = BBusImage.redBooduckBus - case .customized: - self.color = BBusColor.bbusTypeGreen - self.busIcon = BBusImage.greenBooduckBus - case .circulation: - self.color = BBusColor.bbusTypeCirculation - self.busIcon = BBusImage.circulationBooduckBus - case .lateNight: - self.color = BBusColor.bbusTypeBlue - self.busIcon = BBusImage.blueBooduckBus - case .localLine: - self.color = BBusColor.bbusTypeGreen - self.busIcon = BBusImage.greenBooduckBus - } - - self.movingStatusView.configureColor(to: color) - } - - private func fetch() { - self.viewModel?.fetch() - } private func bindErrorMessage() { - self.viewModel?.usecase.$networkError + self.viewModel?.$networkError .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] error in guard let _ = error else { return } @@ -267,11 +161,22 @@ final class MovingStatusViewController: UIViewController { .store(in: &self.cancellables) } + func refresh() { + self.viewModel?.updateAPI() + } + private func networkAlert() { let controller = UIAlertController(title: "네트워크 장애", message: "네트워크 장애가 발생하여 앱이 정상적으로 동작되지 않습니다.", preferredStyle: .alert) let action = UIAlertAction(title: "확인", style: .default, handler: nil) controller.addAction(action) - self.coordinator?.presentAlertToNavigation(controller: controller, completion: nil) + + guard let isFolded = self.viewModel?.isFolded else { return } + if isFolded { + self.coordinator?.presentAlertToNavigation(controller: controller, completion: nil) + } + else { + self.coordinator?.presentAlertToMovingStatus(controller: controller, completion: nil) + } } private func terminateAlert() { @@ -298,7 +203,6 @@ final class MovingStatusViewController: UIViewController { let request = UNNotificationRequest(identifier: Self.alarmIdentifier, content: content, trigger: nil) UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) } - } // MARK: - DataSource: UITableView @@ -355,6 +259,13 @@ extension MovingStatusViewController: EndAlarmButtonDelegate { } } +// MARK: - Delegate: RefreshButton +extension MovingStatusViewController: RefreshButtonDelegate { + func buttonTapped() { + self.refresh() + } +} + // MARK: - Delegate: CLLocation extension MovingStatusViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { diff --git a/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusAPIUsable.swift b/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusAPIUsable.swift new file mode 100644 index 00000000..7be75afb --- /dev/null +++ b/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusAPIUsable.swift @@ -0,0 +1,15 @@ +// +// MovingStatusAPIUsable.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation +import Combine + +protocol MovingStatusAPIUsable: BaseUseCase { + func searchHeader(busRouteId: Int) -> AnyPublisher + func fetchRouteList(busRouteId: Int) -> AnyPublisher<[StationByRouteListDTO], Error> + func fetchBusPosList(busRouteId: Int) -> AnyPublisher<[BusPosByRtidDTO], Error> +} diff --git a/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusAPIUseCase.swift b/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusAPIUseCase.swift new file mode 100644 index 00000000..ab84307c --- /dev/null +++ b/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusAPIUseCase.swift @@ -0,0 +1,51 @@ +// +// MovingStatusAPIUseCase.swift +// BBus +// +// Created by 김태훈 on 2021/11/01. +// + +import Foundation +import Combine + +final class MovingStatusAPIUseCase: MovingStatusAPIUsable { + + private let useCases: GetRouteListUsable & GetStationsByRouteListUsable & GetBusPosByRtidUsable + + init(useCases: GetRouteListUsable & GetStationsByRouteListUsable & GetBusPosByRtidUsable) { + self.useCases = useCases + } + + func searchHeader(busRouteId: Int) -> AnyPublisher { + return self.useCases.getRouteList() + .decode(type: [BusRouteDTO].self, decoder: JSONDecoder()) + .tryMap({ routeList in + let headers = routeList.filter({ $0.routeID == busRouteId }) + if let header = headers.first { + return header + } + else { + throw BBusAPIError.wrongFormatError + } + }) + .eraseToAnyPublisher() + } + + func fetchRouteList(busRouteId: Int) -> AnyPublisher<[StationByRouteListDTO], Error> { + return self.useCases.getStationsByRouteList(busRoutedId: "\(busRouteId)") + .decode(type: StationByRouteResult.self, decoder: JSONDecoder()) + .map({ item in + item.msgBody.itemList + }) + .eraseToAnyPublisher() + } + + func fetchBusPosList(busRouteId: Int) -> AnyPublisher<[BusPosByRtidDTO], Error> { + return self.useCases.getBusPosByRtid(busRoutedId: "\(busRouteId)") + .decode(type: BusPosByRtidResult.self, decoder: JSONDecoder()) + .tryMap ({ item in + return item.msgBody.itemList + }) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusCalculatable.swift b/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusCalculatable.swift new file mode 100644 index 00000000..fc0ea060 --- /dev/null +++ b/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusCalculatable.swift @@ -0,0 +1,20 @@ +// +// MovingStatusCalculatable.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation + +protocol MovingStatusCalculatable: AverageSectionTimeCalculatable { + func filteredBuses(from buses: [BusPosByRtidDTO], startOrd: Int, currentOrd: Int, count: Int) -> [BusPosByRtidDTO] + func convertBusInfo(header: BusRouteDTO) -> BusInfo + func remainStation(bus: BusPosByRtidDTO, startOrd: Int, count: Int) -> Int + func pushAlarmMessage(remainStation: Int) -> (message: String?, terminated: Bool) + func remainTime(bus: BusPosByRtidDTO, stations: [StationInfo], startOrd: Int, boardedBus: BoardedBus) -> Int + func convertBusPos(startOrd: Int, order: Int, sect: String, fullSect: String) -> Double + func isOnBoard(gpsY: Double, gpsX: Double, busY: Double, busX: Double) -> Bool + func stationIndex(with targetId: String, with stations: [StationByRouteListDTO]) -> Int? + func filteredStations(from stations: [StationByRouteListDTO]) -> (stations: [StationInfo], time: Int) +} diff --git a/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusCalculateUseCase.swift b/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusCalculateUseCase.swift new file mode 100644 index 00000000..b709fdab --- /dev/null +++ b/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusCalculateUseCase.swift @@ -0,0 +1,97 @@ +// +// MovingStatusCalculateUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/11/29. +// + +import Foundation +import Combine +import CoreLocation + +final class MovingStatusCalculateUseCase: MovingStatusCalculatable { + + func filteredBuses(from buses: [BusPosByRtidDTO], startOrd: Int, currentOrd: Int, count: Int) -> [BusPosByRtidDTO] { + return buses.filter { $0.sectionOrder >= currentOrd && $0.sectionOrder < startOrd + count } + } + + func convertBusInfo(header: BusRouteDTO) -> BusInfo { + let busInfo: BusInfo + busInfo.busName = header.busRouteName + busInfo.type = header.routeType + + return busInfo + } + + func remainStation(bus: BusPosByRtidDTO, startOrd: Int, count: Int) -> Int { + return (count - 1) - (bus.sectionOrder - startOrd) + } + + func pushAlarmMessage(remainStation: Int) -> (message: String?, terminated: Bool) { + if remainStation < 4 && remainStation > 1 { + return ("\(remainStation) 정거장 남았어요!", false) + } + else if remainStation == 1 { + return ("다음 정거장에 내려야 합니다!", false) + } + else if remainStation <= 0 { + return ("하차 정거장에 도착하여 알림이 종료되었습니다.", true) + } + else { + return (nil, false) + } + } + + func remainTime(bus: BusPosByRtidDTO, stations: [StationInfo], startOrd: Int, boardedBus: BoardedBus) -> Int { + let currentIdx = (bus.sectionOrder - startOrd) + var totalRemainTime = 0 + + for index in currentIdx...stations.count-1 { + totalRemainTime += stations[index].sectTime + } + + let currentLocation = boardedBus.location + let extraPersent = Double(currentLocation) - Double(currentIdx) + let extraTime = extraPersent * Double(stations[currentIdx].sectTime) + + return totalRemainTime - Int(ceil(extraTime)) + } + + func convertBusPos(startOrd: Int, order: Int, sect: String, fullSect: String) -> Double { + let order = Double(order - startOrd) + let sect = Double((sect as NSString).floatValue) + let fullSect = Double((fullSect as NSString).floatValue) + return order + (sect/fullSect) + } + + func isOnBoard(gpsY: Double, gpsX: Double, busY: Double, busX: Double) -> Bool { + let userLocation = CLLocation(latitude: gpsX, longitude: gpsY) + let busLocation = CLLocation(latitude: busX, longitude: busY) + let distanceInMeters = userLocation.distance(from: busLocation) + + return distanceInMeters <= 100.0 + } + + func stationIndex(with targetId: String, with stations: [StationByRouteListDTO]) -> Int? { + return stations.firstIndex(where: { $0.arsId == targetId }) + } + + func filteredStations(from stations: [StationByRouteListDTO]) -> (stations: [StationInfo], time: Int) { + var resultStations: [StationInfo] = [] + var totalTime: Int = 0 + + for (idx, station) in stations.enumerated() { + let info: StationInfo + info.speed = station.sectionSpeed + info.afterSpeed = idx+1 == stations.count ? nil : stations[idx+1].sectionSpeed + info.count = stations.count + info.title = station.stationName + info.sectTime = idx == 0 ? 0 : self.averageSectionTime(speed: info.speed, distance: station.fullSectionDistance) + + resultStations.append(info) + totalTime += info.sectTime + } + + return (resultStations, totalTime) + } +} diff --git a/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusUseCase.swift b/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusUseCase.swift deleted file mode 100644 index 81540330..00000000 --- a/BBus/BBus/Foreground/MovingStatus/UseCase/MovingStatusUseCase.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// MovingStatusUseCase.swift -// BBus -// -// Created by 김태훈 on 2021/11/01. -// - -import Foundation -import Combine - -final class MovingStatusUsecase { - - private let usecases: GetRouteListUsecase & GetStationsByRouteListUsecase & GetBusPosByRtidUsecase - @Published var header: BusRouteDTO? - @Published var buses: [BusPosByRtidDTO] = [] - @Published var stations: [StationByRouteListDTO] = [] - @Published var networkError: Error? - - private var cancellables: Set - static let queue = DispatchQueue(label: "MovingStatus") - - init(usecases: GetRouteListUsecase & GetStationsByRouteListUsecase & GetBusPosByRtidUsecase) { - self.usecases = usecases - self.cancellables = [] - self.networkError = nil - } - - func searchHeader(busRouteId: Int) { - Self.queue.async { - self.usecases.getRouteList() - .receive(on: Self.queue) - .decode(type: [BusRouteDTO].self, decoder: JSONDecoder()) - .tryMap({ routeList in - let headers = routeList.filter({ $0.routeID == busRouteId }) - if let header = headers.first { - return header - } - else { - throw BBusAPIError.wrongFormatError - } - }) - .retry({ [weak self] in - self?.searchHeader(busRouteId: busRouteId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$header) - } - } - - func fetchRouteList(busRouteId: Int) { - Self.queue.async { - self.usecases.getStationsByRouteList(busRoutedId: "\(busRouteId)") - .receive(on: Self.queue) - .decode(type: StationByRouteResult.self, decoder: JSONDecoder()) - .retry ({ [weak self] in - self?.fetchRouteList(busRouteId: busRouteId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .map({ item in - item.msgBody.itemList - }) - .assign(to: &self.$stations) - } - } - - func fetchBusPosList(busRouteId: Int) { - Self.queue.async { - self.usecases.getBusPosByRtid(busRoutedId: "\(busRouteId)") - .receive(on: Self.queue) - .decode(type: BusPosByRtidResult.self, decoder: JSONDecoder()) - .tryMap ({ item in - return item.msgBody.itemList - }) - .retry ({ [weak self] in - self?.fetchBusPosList(busRouteId: busRouteId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$buses) - } - } -} diff --git a/BBus/BBus/Foreground/MovingStatus/View/MovingStatusView.swift b/BBus/BBus/Foreground/MovingStatus/View/MovingStatusView.swift index cc28eb73..be4c5bd5 100644 --- a/BBus/BBus/Foreground/MovingStatus/View/MovingStatusView.swift +++ b/BBus/BBus/Foreground/MovingStatus/View/MovingStatusView.swift @@ -19,7 +19,7 @@ protocol EndAlarmButtonDelegate: AnyObject { func shouldEndAlarm() } -final class MovingStatusView: UIView { +final class MovingStatusView: RefreshableView { static let bottomIndicatorHeight: CGFloat = 80 static let endAlarmViewHeight: CGFloat = 80 @@ -129,8 +129,12 @@ final class MovingStatusView: UIView { }() private lazy var loader: UIActivityIndicatorView = { let loader = UIActivityIndicatorView(style: .large) + loader.color = BBusColor.gray return loader }() + private var busTag: MovingStatusBusTagView? + private var color: UIColor? + private var busIcon: UIImage? required init?(coder: NSCoder) { super.init(coder: coder) @@ -151,8 +155,8 @@ final class MovingStatusView: UIView { } // MARK: - Configure - private func configureLayout() { - self.addSubviews(self.bottomIndicatorButton, self.endAlarmButton, self.stationsTableView, self.headerView, self.loader) + override func configureLayout() { + self.addSubviews(self.bottomIndicatorButton, self.endAlarmButton, self.stationsTableView, self.headerView, self.loader, self.refreshButton) NSLayoutConstraint.activate([ self.bottomIndicatorButton.topAnchor.constraint(equalTo: self.topAnchor), @@ -263,14 +267,26 @@ final class MovingStatusView: UIView { self.loader.centerXAnchor.constraint(equalTo: self.centerXAnchor), self.loader.centerYAnchor.constraint(equalTo: self.centerYAnchor) ]) + + let refreshButtonWidthAnchor: CGFloat = 50 + let refreshBottomInterval: CGFloat = -MovingStatusView.endAlarmViewHeight + let refreshTrailingInterval: CGFloat = -16 + + NSLayoutConstraint.activate([ + self.refreshButton.widthAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), + self.refreshButton.heightAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), + self.refreshButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: refreshTrailingInterval), + self.refreshButton.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: refreshBottomInterval) + ]) } - func configureDelegate(_ delegate: UITableViewDelegate & UITableViewDataSource & BottomIndicatorButtonDelegate & FoldButtonDelegate & EndAlarmButtonDelegate) { + func configureDelegate(_ delegate: UITableViewDelegate & UITableViewDataSource & BottomIndicatorButtonDelegate & FoldButtonDelegate & EndAlarmButtonDelegate & RefreshButtonDelegate) { self.stationsTableView.delegate = delegate self.stationsTableView.dataSource = delegate self.bottomIndicatorButtondelegate = delegate self.foldButtonDelegate = delegate self.endAlarmButtonDelegate = delegate + self.refreshButton.configureDelegate(delegate) } func createBusTag(location: CGFloat = 0, color: UIColor? = BBusColor.gray, busIcon: UIImage? = BBusImage.blueBusIcon, remainStation: Int?) -> MovingStatusBusTagView { @@ -291,6 +307,37 @@ final class MovingStatusView: UIView { return busTag } + func configureColorAndBusIcon(type: RouteType) { + switch type { + case .mainLine: + self.color = BBusColor.bbusTypeBlue + self.busIcon = BBusImage.blueBooduckBus + case .broadArea: + self.color = BBusColor.bbusTypeRed + self.busIcon = BBusImage.redBooduckBus + case .customized: + self.color = BBusColor.bbusTypeGreen + self.busIcon = BBusImage.greenBooduckBus + case .circulation: + self.color = BBusColor.bbusTypeCirculation + self.busIcon = BBusImage.circulationBooduckBus + case .lateNight: + self.color = BBusColor.bbusTypeBlue + self.busIcon = BBusImage.blueBooduckBus + case .localLine: + self.color = BBusColor.bbusTypeGreen + self.busIcon = BBusImage.greenBooduckBus + case .town: + self.color = BBusColor.bbusTypeGreen + self.busIcon = BBusImage.greenBusIcon + case .airport: + self.color = BBusColor.bbusLikeYellow + self.busIcon = BBusImage.blueBusIcon + } + + self.configureColor(to: self.color) + } + func configureColor(to color: UIColor?) { self.bottomIndicatorButton.backgroundColor = color self.busNumberLabel.textColor = color @@ -320,6 +367,22 @@ final class MovingStatusView: UIView { self.bottomIndicatorLabel.text = headerInfoResult self.alarmStatusLabel.text = headerInfoResult } + + func configureBusTag(bus: BoardedBus? = nil) { + self.busTag?.removeFromSuperview() + + if let bus = bus { + self.busTag = self.createBusTag(location: bus.location, + color: self.color, + busIcon: self.busIcon, + remainStation: bus.remainStation) + } + else { + self.busTag = self.createBusTag(color: self.color, + busIcon: self.busIcon, + remainStation: nil) + } + } func reload() { self.stationsTableView.reloadData() diff --git a/BBus/BBus/Foreground/MovingStatus/ViewModel/MovingStatusViewModel.swift b/BBus/BBus/Foreground/MovingStatus/ViewModel/MovingStatusViewModel.swift index 6ff15d2a..8df3b081 100644 --- a/BBus/BBus/Foreground/MovingStatus/ViewModel/MovingStatusViewModel.swift +++ b/BBus/BBus/Foreground/MovingStatus/ViewModel/MovingStatusViewModel.swift @@ -8,7 +8,6 @@ import Foundation import Combine import CoreGraphics -import CoreLocation typealias BusInfo = (busName: String, type: RouteType) typealias BoardedBus = (location: CGFloat, remainStation: Int?) @@ -16,26 +15,29 @@ typealias StationInfo = (speed: Int, afterSpeed: Int?, count: Int, title: String final class MovingStatusViewModel { - let usecase: MovingStatusUsecase + let apiUseCase: MovingStatusAPIUsable + let calculateUseCase: MovingStatusCalculatable private var cancellables: Set private let busRouteId: Int private let fromArsId: String private let toArsId: String - private var startOrd: Int? // 2 + private var startOrd: Int? private var currentOrd: Int? private(set) var isFolded: Bool = false @Published private(set) var isterminated: Bool = false - @Published private(set) var busInfo: BusInfo? // 1 - @Published private(set) var stationInfos: [StationInfo] = [] // 3 - @Published private(set) var buses: [BusPosByRtidDTO] = [] // 5 - @Published private(set) var remainingTime: Int? // 4, 7 - @Published private(set) var remainingStation: Int? // 6 - @Published private(set) var boardedBus: BoardedBus? // 8 + @Published private(set) var busInfo: BusInfo? + @Published private(set) var stationInfos: [StationInfo] = [] + @Published private(set) var buses: [BusPosByRtidDTO] = [] + @Published private(set) var remainingTime: Int? + @Published private(set) var remainingStation: Int? + @Published private(set) var boardedBus: BoardedBus? @Published private(set) var message: String? @Published private(set) var stopLoader: Bool = false + @Published private(set) var networkError: Error? - init(usecase: MovingStatusUsecase, busRouteId: Int, fromArsId: String, toArsId: String) { - self.usecase = usecase + init(apiUseCase: MovingStatusAPIUsable, calculateUseCase: MovingStatusCalculatable, busRouteId: Int, fromArsId: String, toArsId: String) { + self.apiUseCase = apiUseCase + self.calculateUseCase = calculateUseCase self.busRouteId = busRouteId self.fromArsId = fromArsId self.toArsId = toArsId @@ -45,6 +47,33 @@ final class MovingStatusViewModel { self.configureObserver() } + func updateAPI() { + self.bindBusesPosInfo() + } + + func fold() { + self.isFolded = true + } + + func unfold() { + self.isFolded = false + } + + // Background 내에서 GPS 변화시 불리는 함수 + func findBoardBus(gpsY: Double, gpsX: Double) { + if buses.isEmpty { return } + if stationInfos.isEmpty { return } + + for bus in buses { + if self.calculateUseCase.isOnBoard(gpsY: gpsY, gpsX: gpsX, busY: bus.gpsY, busX: bus.gpsX) { + self.updateRemainingStation(bus: bus) + self.updateBoardBus(bus: bus) + self.updateRemainingTime(bus: bus) + break + } + } + } + private func configureObserver() { NotificationCenter.default.addObserver(forName: .fifteenSecondsPassed, object: nil, queue: .none) { [weak self] _ in guard let self = self else { return } @@ -58,74 +87,76 @@ final class MovingStatusViewModel { self.bindLoader() self.bindHeaderInfo() self.bindStationsInfo() - self.bindBusesPosInfo() } private func bindHeaderInfo() { - self.usecase.$header - .receive(on: MovingStatusUsecase.queue) + self.apiUseCase.searchHeader(busRouteId: self.busRouteId) + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + self?.networkError = error + }) .sink(receiveValue: { [weak self] header in - self?.convertBusInfo(header: header) + guard let self = self, + let header = header else { return } + + self.busInfo = self.calculateUseCase.convertBusInfo(header: header) }) .store(in: &self.cancellables) } private func bindStationsInfo() { - self.usecase.$stations - .receive(on: MovingStatusUsecase.queue) + self.apiUseCase.fetchRouteList(busRouteId: self.busRouteId) + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + self?.networkError = error + }) .sink(receiveValue: { [weak self] stations in self?.convertBusStations(with: stations) + self?.bindBusesPosInfo() }) .store(in: &self.cancellables) } private func bindBusesPosInfo() { - self.usecase.$buses - .receive(on: MovingStatusUsecase.queue) + self.apiUseCase.fetchBusPosList(busRouteId: self.busRouteId) + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + self?.networkError = error + }) .sink { [weak self] buses in - guard let currentOrd = self?.currentOrd, - let startOrd = self?.startOrd, - let count = self?.stationInfos.count else { return } - - self?.buses = buses.filter { $0.sectionOrder >= currentOrd && $0.sectionOrder < startOrd + count } // 5 - // Test 로직 - guard let y = self?.buses.first?.gpsY, - let x = self?.buses.first?.gpsX else { return } - - self?.findBoardBus(gpsY: y, gpsX: x) + guard let self = self, + let currentOrd = self.currentOrd, + let startOrd = self.startOrd else { return } + let count = self.stationInfos.count + + self.buses = self.calculateUseCase.filteredBuses(from: buses, + startOrd: startOrd, + currentOrd: currentOrd, + count: count) + + /* Test 용 사용자 gps 조작 코드 + guard let y = self.buses.first?.gpsY, + let x = self.buses.first?.gpsX else { return } + + self.findBoardBus(gpsY: y, gpsX: x) */ } .store(in: &self.cancellables) } - - private func convertBusInfo(header: BusRouteDTO?) { - guard let header = header else { return } - - let busInfo: BusInfo - busInfo.busName = header.busRouteName - busInfo.type = header.routeType - - self.busInfo = busInfo // 1 - } - - // Background 내에서 GPS 변화시 불리는 함수 - func findBoardBus(gpsY: Double, gpsX: Double) { - if buses.isEmpty { return } - if stationInfos.isEmpty { return } - - for bus in buses { - if self.isOnBoard(gpsY: gpsY, gpsX: gpsX, busY: bus.gpsY, busX: bus.gpsX) { - self.updateRemainingStation(bus: bus) - self.updateBoardBus(bus: bus) - self.updateRemainingTime(bus: bus) - break - } - } + + private func bindLoader() { + self.$busInfo.zip(self.$stationInfos) + .dropFirst() + .sink(receiveValue: { _ in + self.stopLoader = true + }) + .store(in: &self.cancellables) } - // 남은 정거장 수 업데이트 로직 private func updateRemainingStation(bus: BusPosByRtidDTO) { guard let startOrd = self.startOrd else { return } - let remainStation = (self.stationInfos.count - 1) - (bus.sectionOrder - startOrd) // 6 + let remainStation = self.calculateUseCase.remainStation(bus: bus, + startOrd: startOrd, + count: self.stationInfos.count) if self.remainingStation != remainStation { self.pushAlarm(remainStation: remainStation) @@ -133,129 +164,48 @@ final class MovingStatusViewModel { } } - // 정거장 수가 변화되었을 경우 알람 푸쉬 로직 private func pushAlarm(remainStation: Int) { - if remainStation < 4 && remainStation > 1 { - self.message = "\(remainStation) 정거장 남았어요!" - } - else if remainStation == 1 { - self.message = "다음 정거장에 내려야 합니다!" - } - else if remainStation <= 0 { - self.message = "하차 정거장에 도착하여 알람이 종료되었습니다." + let result = self.calculateUseCase.pushAlarmMessage(remainStation: remainStation) + guard let message = result.message else { return } + + self.message = message + if result.terminated { self.isterminated = true } } - // 남은 시간 업데이트 로직 private func updateRemainingTime(bus: BusPosByRtidDTO) { guard let startOrd = self.startOrd, let boardedBus = self.boardedBus else { return } - let currentIdx = (bus.sectionOrder - startOrd) - var totalRemainTime = 0 - for index in currentIdx...self.stationInfos.count-1 { - totalRemainTime += self.stationInfos[index].sectTime - } - - let currentLocation = boardedBus.location - let extraPersent = Double(currentLocation) - Double(currentIdx) - let extraTime = extraPersent * Double(self.stationInfos[currentIdx].sectTime) - totalRemainTime -= Int(ceil(extraTime)) - - self.remainingTime = totalRemainTime // 7 + self.remainingTime = self.calculateUseCase.remainTime(bus: bus, + stations: self.stationInfos, + startOrd: startOrd, + boardedBus: boardedBus) } - // 탑승한 버스 업데이트 로직 private func updateBoardBus(bus: BusPosByRtidDTO) { guard let startOrd = self.startOrd else { return } self.currentOrd = bus.sectionOrder let boardedBus: BoardedBus - boardedBus.location = self.convertBusPos(startOrd: startOrd, - order: bus.sectionOrder, - sect: bus.sectDist, - fullSect: bus.fullSectDist) + boardedBus.location = CGFloat(self.calculateUseCase.convertBusPos(startOrd: startOrd, + order: bus.sectionOrder, + sect: bus.sectDist, + fullSect: bus.fullSectDist)) boardedBus.remainStation = self.remainingStation - self.boardedBus = boardedBus // 8 - } - - // Bus - 유저간 거리 측정 로직 - func isOnBoard(gpsY: Double, gpsX: Double, busY: Double, busX: Double) -> Bool { - let userLocation = CLLocation(latitude: gpsX, longitude: gpsY) - let busLocation = CLLocation(latitude: busX, longitude: busY) - let distanceInMeters = userLocation.distance(from: busLocation) - - return distanceInMeters <= 100.0 - } - - // 현재 버스의 노선도 위치 반환 - private func convertBusPos(startOrd: Int, order: Int, sect: String, fullSect: String) -> CGFloat { - let order = CGFloat(order - startOrd) - let sect = CGFloat((sect as NSString).floatValue) - let fullSect = CGFloat((fullSect as NSString).floatValue) - - return order + (sect/fullSect) + self.boardedBus = boardedBus } - + private func convertBusStations(with stations: [StationByRouteListDTO]) { - guard let startIndex = stations.firstIndex(where: { $0.arsId == self.fromArsId }) else { return } - guard let endIndex = stations.firstIndex(where: { $0.arsId == self.toArsId }) else { return } - - var stationsResult: [StationInfo] = [] - var totalTime: Int = 0 + guard let startIndex = self.calculateUseCase.stationIndex(with: self.fromArsId, with: stations) else { return } + guard let endIndex = self.calculateUseCase.stationIndex(with: self.toArsId, with: stations) else { return } + let stations = Array(stations[startIndex...endIndex]) - self.startOrd = stations.first?.sequence // 2 + self.startOrd = stations.first?.sequence self.currentOrd = self.startOrd - - for (idx, station) in stations.enumerated() { - let info: StationInfo - info.speed = station.sectionSpeed - info.afterSpeed = idx+1 == stations.count ? nil : stations[idx+1].sectionSpeed - info.count = stations.count - info.title = station.stationName - info.sectTime = idx == 0 ? 0 : Self.averageSectionTime(speed: info.speed, distance: station.fullSectionDistance) - - stationsResult.append(info) - totalTime += info.sectTime - } - - self.stationInfos = stationsResult // 3 - self.remainingTime = totalTime // 4 - } - - static func averageSectionTime(speed: Int, distance: Int) -> Int { - let averageBusSpeed: Double = 21 - let metterToKilometter: Double = 0.06 - - let result = Double(distance)/averageBusSpeed*metterToKilometter - return Int(ceil(result)) - } - - func fetch() { - self.usecase.searchHeader(busRouteId: self.busRouteId) - self.usecase.fetchRouteList(busRouteId: self.busRouteId) - self.usecase.fetchBusPosList(busRouteId: self.busRouteId) - } - - // 타이머가 일정주기로 실행 - func updateAPI() { - self.usecase.fetchBusPosList(busRouteId: self.busRouteId) //고민 필요 - } - - func fold() { - self.isFolded = true - } - - func unfold() { - self.isFolded = false - } - - private func bindLoader() { - self.$busInfo.zip(self.$stationInfos) - .dropFirst() - .sink(receiveValue: { _ in - self.stopLoader = true - }) - .store(in: &self.cancellables) + + let results = self.calculateUseCase.filteredStations(from: stations) + self.stationInfos = results.stations + self.remainingTime = results.time } } diff --git a/BBus/BBus/Foreground/Search/SearchCoordinator.swift b/BBus/BBus/Foreground/Search/SearchCoordinator.swift index a4aa9392..d38d0df8 100644 --- a/BBus/BBus/Foreground/Search/SearchCoordinator.swift +++ b/BBus/BBus/Foreground/Search/SearchCoordinator.swift @@ -16,8 +16,13 @@ final class SearchCoordinator: BusRoutePushable, StationPushable { } func start() { - let usecase = SearchUseCase(usecases: BBusAPIUsecases(on: SearchUseCase.queue)) - let viewModel = SearchViewModel(usecase: usecase) + let apiUseCases = BBusAPIUseCases(networkService: NetworkService(), + persistenceStorage: PersistenceStorage(), + tokenManageType: TokenManager.self, + requestFactory: RequestFactory()) + let apiUseCase = SearchAPIUseCase(useCases: apiUseCases) + let calculateUseCase = SearchCalculateUseCase() + let viewModel = SearchViewModel(apiUseCase: apiUseCase, calculateUseCase: calculateUseCase) let viewController = SearchViewController(viewModel: viewModel) viewController.coordinator = self self.navigationPresenter.pushViewController(viewController, animated: true) diff --git a/BBus/BBus/Foreground/Search/SearchViewController.swift b/BBus/BBus/Foreground/Search/SearchViewController.swift index 786810fd..aabe45c8 100644 --- a/BBus/BBus/Foreground/Search/SearchViewController.swift +++ b/BBus/BBus/Foreground/Search/SearchViewController.swift @@ -8,23 +8,14 @@ import UIKit import Combine -final class SearchViewController: UIViewController { - +final class SearchViewController: UIViewController, BaseViewControllerType { + weak var coordinator: SearchCoordinator? - private lazy var searchView = SearchView() private let viewModel: SearchViewModel? + private lazy var searchView = SearchView() + private var cancellables: Set = [] - override func viewDidLoad() { - super.viewDidLoad() - - self.configureLayout() - self.configureUI() - self.configureDelegate() - self.binding() - self.searchView.configureInitialTabStatus(type: .bus) - } - init(viewModel: SearchViewModel) { self.viewModel = viewModel super.init(nibName: nil, bundle: nil) @@ -34,14 +25,17 @@ final class SearchViewController: UIViewController { self.viewModel = nil super.init(coder: coder) } - - // MARK: - Configuration - private func configureDelegate() { - self.searchView.configureBackButtonDelegate(self) - self.searchView.configureDelegate(self) + + override func viewDidLoad() { + super.viewDidLoad() + self.baseViewDidLoad() + + self.configureColor() + self.searchView.configureInitialTabStatus(type: .bus) } - private func configureLayout() { + // MARK: - Configuration + func configureLayout() { self.view.addSubviews(self.searchView) NSLayoutConstraint.activate([ @@ -51,12 +45,24 @@ final class SearchViewController: UIViewController { self.searchView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor) ]) } + + func configureDelegate() { + self.searchView.configureBackButtonDelegate(self) + self.searchView.configureDelegate(self) + } + + func refresh() { } + + func bindAll() { + self.bindSearchResults() + self.bindNetworkError() + } - private func configureUI() { + private func configureColor() { self.view.backgroundColor = BBusColor.white } - private func binding() { + private func bindSearchResults() { self.viewModel?.$searchResults .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] response in @@ -67,8 +73,10 @@ final class SearchViewController: UIViewController { self?.searchView.reload() }) .store(in: &self.cancellables) - - self.viewModel?.usecase.$networkError + } + + private func bindNetworkError() { + self.viewModel?.$networkError .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] error in guard let _ = error else { return } diff --git a/BBus/BBus/Foreground/Search/UseCase/SearchAPIUsable.swift b/BBus/BBus/Foreground/Search/UseCase/SearchAPIUsable.swift new file mode 100644 index 00000000..86a295ae --- /dev/null +++ b/BBus/BBus/Foreground/Search/UseCase/SearchAPIUsable.swift @@ -0,0 +1,14 @@ +// +// SearchAPIUsable.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation +import Combine + +protocol SearchAPIUsable: BaseUseCase { + func loadBusRouteList() -> AnyPublisher<[BusRouteDTO], Error> + func loadStationList() -> AnyPublisher<[StationDTO], Error> +} diff --git a/BBus/BBus/Foreground/Search/UseCase/SearchAPIUseCase.swift b/BBus/BBus/Foreground/Search/UseCase/SearchAPIUseCase.swift new file mode 100644 index 00000000..4878c1ab --- /dev/null +++ b/BBus/BBus/Foreground/Search/UseCase/SearchAPIUseCase.swift @@ -0,0 +1,34 @@ +// +// SearchAPIUseCase.swift +// BBus +// +// Created by 김태훈 on 2021/11/01. +// + +import Foundation +import Combine + +final class SearchAPIUseCase: SearchAPIUsable { + + private let useCases: GetRouteListUsable & GetStationListUsable + private var cancellables: Set + @Published var networkError: Error? + + init(useCases: GetRouteListUsable & GetStationListUsable) { + self.useCases = useCases + self.cancellables = [] + } + + func loadBusRouteList() -> AnyPublisher<[BusRouteDTO], Error> { + self.useCases.getRouteList() + .decode(type: [BusRouteDTO].self, decoder: JSONDecoder()) + .eraseToAnyPublisher() + } + + func loadStationList() -> AnyPublisher<[StationDTO], Error> { + self.useCases.getStationList() + .decode(type: [StationDTO].self, decoder: JSONDecoder()) + .eraseToAnyPublisher() + } + +} diff --git a/BBus/BBus/Foreground/Search/UseCase/SearchCalculatable.swift b/BBus/BBus/Foreground/Search/UseCase/SearchCalculatable.swift new file mode 100644 index 00000000..c0ad14f8 --- /dev/null +++ b/BBus/BBus/Foreground/Search/UseCase/SearchCalculatable.swift @@ -0,0 +1,13 @@ +// +// SearchCalculatable.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation + +protocol SearchCalculatable { + func searchBus(by keyword: String, at routeList: [BusRouteDTO]) -> [BusSearchResult] + func searchStation(by keyword: String, at stationList: [StationDTO]) -> [StationSearchResult] +} diff --git a/BBus/BBus/Foreground/Search/UseCase/SearchCalculateUseCase.swift b/BBus/BBus/Foreground/Search/UseCase/SearchCalculateUseCase.swift new file mode 100644 index 00000000..a86f3e0c --- /dev/null +++ b/BBus/BBus/Foreground/Search/UseCase/SearchCalculateUseCase.swift @@ -0,0 +1,35 @@ +// +// SearchCalculateUseCase.swift +// BBus +// +// Created by 최수정 on 2021/11/30. +// + +import Foundation + +final class SearchCalculateUseCase: SearchCalculatable { + func searchBus(by keyword: String, at routeList: [BusRouteDTO]) -> [BusSearchResult] { + if keyword.isEmpty { + return [] + } + else { + return routeList + .filter { $0.busRouteName.hasPrefix(keyword) } + .map { BusSearchResult(busRouteDTO: $0) } + } + } + + func searchStation(by keyword: String, at stationList: [StationDTO]) -> [StationSearchResult] { + if keyword.isEmpty { + return [] + } + else { + return stationList + .map { StationSearchResult(stationName: $0.stationName, + arsId: $0.arsID, + stationNameMatchRanges: $0.stationName.ranges(of: keyword), + arsIdMatchRanges: $0.arsID.ranges(of: keyword)) } + .filter { !($0.arsIdMatchRanges.isEmpty && $0.stationNameMatchRanges.isEmpty) } + } + } +} diff --git a/BBus/BBus/Foreground/Search/UseCase/SearchUseCase.swift b/BBus/BBus/Foreground/Search/UseCase/SearchUseCase.swift deleted file mode 100644 index 65ac6710..00000000 --- a/BBus/BBus/Foreground/Search/UseCase/SearchUseCase.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// SearchUseCase.swift -// BBus -// -// Created by 김태훈 on 2021/11/01. -// - -import Foundation -import Combine - -final class SearchUseCase { - - private let usecases: GetRouteListUsecase & GetStationListUsecase - @Published var routeList: [BusRouteDTO] - @Published var stationList: [StationDTO] - @Published var networkError: Error? - private var cancellables: Set - static let queue = DispatchQueue(label: "Search") - - init(usecases: GetRouteListUsecase & GetStationListUsecase) { - self.usecases = usecases - self.routeList = [] - self.stationList = [] - self.networkError = nil - self.cancellables = [] - self.startSearch() - } - - private func startSearch() { - self.startRouteSearch() - self.startStationSearch() - } - - private func startRouteSearch() { - Self.queue.async { - self.usecases.getRouteList() - .receive(on: Self.queue) - .decode(type: [BusRouteDTO].self, decoder: JSONDecoder()) - .retry({ [weak self] in - self?.startRouteSearch() - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$routeList) - } - } - - private func startStationSearch() { - Self.queue.async { - self.usecases.getStationList() - .receive(on: Self.queue) - .decode(type: [StationDTO].self, decoder: JSONDecoder()) - .retry({ [weak self] in - self?.startStationSearch() - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$stationList) - } - } - - func searchBus(by keyword: String) -> [BusSearchResult] { - if keyword == "" { - return [] - } - else { - return routeList.filter { $0.busRouteName.hasPrefix(keyword) } - .map { BusSearchResult(busRouteDTO: $0) } - } - } - - func searchStation(by keyword: String) -> [StationSearchResult] { - if keyword == "" { - return [] - } - else { - return stationList.map { StationSearchResult(stationName: $0.stationName, - arsId: $0.arsID, - stationNameMatchRanges: $0.stationName.ranges(of: keyword), - arsIdMatchRanges: $0.arsID.ranges(of: keyword)) } - .filter { !($0.arsIdMatchRanges.isEmpty && $0.stationNameMatchRanges.isEmpty) } - } - } -} diff --git a/BBus/BBus/Foreground/Search/View/SearchResultCollectionViewCell.swift b/BBus/BBus/Foreground/Search/View/SearchResultCollectionViewCell.swift index 8857380d..d4571538 100644 --- a/BBus/BBus/Foreground/Search/View/SearchResultCollectionViewCell.swift +++ b/BBus/BBus/Foreground/Search/View/SearchResultCollectionViewCell.swift @@ -73,6 +73,10 @@ final class SearchResultCollectionViewCell: UICollectionViewCell { self.titleLabel.textColor = BBusColor.bbusTypeBlue case .localLine: self.titleLabel.textColor = BBusColor.bbusTypeGreen + case .town: + self.titleLabel.textColor = BBusColor.bbusTypeGreen + case .airport: + self.titleLabel.textColor = BBusColor.bbusLikeYellow } } diff --git a/BBus/BBus/Foreground/Search/ViewModel/SearchViewModel.swift b/BBus/BBus/Foreground/Search/ViewModel/SearchViewModel.swift index ddbee73a..0181f82d 100644 --- a/BBus/BBus/Foreground/Search/ViewModel/SearchViewModel.swift +++ b/BBus/BBus/Foreground/Search/ViewModel/SearchViewModel.swift @@ -9,34 +9,69 @@ import Foundation import Combine final class SearchViewModel { - - typealias DecoratedBusResult = (busRouteName: NSMutableAttributedString, routeType: NSMutableAttributedString, routeId: Int) - let usecase: SearchUseCase + private let apiUseCase: SearchAPIUsable + private let calculateUseCase: SearchCalculatable + private var busRouteList: [BusRouteDTO] + private var stationList: [StationDTO] @Published private var keyword: String @Published private(set) var searchResults: SearchResults + @Published private(set) var networkError: Error? private var cancellables: Set - init(usecase: SearchUseCase) { - self.usecase = usecase + init(apiUseCase: SearchAPIUsable, calculateUseCase: SearchCalculatable) { + self.apiUseCase = apiUseCase + self.calculateUseCase = calculateUseCase self.keyword = "" + self.busRouteList = [] + self.stationList = [] self.searchResults = SearchResults(busSearchResults: [], stationSearchResults: []) self.cancellables = [] - self.prepare() + + self.bind() } func configure(keyword: String) { self.keyword = keyword } + + private func bind() { + self.bindBusRouteList() + self.bindStationList() + self.bindKeyword() + } + + private func bindBusRouteList() { + self.apiUseCase.loadBusRouteList() + .catchError { [weak self] error in + self?.networkError = error + } + .sink { [weak self] busRouteList in + self?.busRouteList = busRouteList + } + .store(in: &self.cancellables) + } + + private func bindStationList() { + self.apiUseCase.loadStationList() + .catchError { [weak self] error in + self?.networkError = error + } + .sink { [weak self] stationList in + self?.stationList = stationList + } + .store(in: &self.cancellables) + } - private func prepare() { + private func bindKeyword() { self.$keyword - .receive(on: SearchUseCase.queue) - .debounce(for: .milliseconds(400), scheduler: SearchUseCase.queue) + .debounce(for: .milliseconds(400), scheduler: DispatchQueue.global()) .sink { [weak self] keyword in guard let self = self else { return } - self.searchResults.busSearchResults = self.usecase.searchBus(by: keyword) - self.searchResults.stationSearchResults = self.usecase.searchStation(by: keyword) + + let busSearchResults = self.calculateUseCase.searchBus(by: keyword, at: self.busRouteList) + let stationSearchResults = self.calculateUseCase.searchStation(by: keyword, at: self.stationList) + self.searchResults = SearchResults(busSearchResults: busSearchResults, stationSearchResults: stationSearchResults) } .store(in: &self.cancellables) } diff --git a/BBus/BBus/Foreground/Station/StationCoordinator.swift b/BBus/BBus/Foreground/Station/StationCoordinator.swift index 0be95a95..20d64f48 100644 --- a/BBus/BBus/Foreground/Station/StationCoordinator.swift +++ b/BBus/BBus/Foreground/Station/StationCoordinator.swift @@ -16,8 +16,15 @@ final class StationCoordinator: BusRoutePushable, AlarmSettingPushable { } func start(arsId: String) { - let usecase = StationUsecase(usecases: BBusAPIUsecases(on: StationUsecase.queue)) - let viewModel = StationViewModel(usecase: usecase, arsId: arsId) + let apiUseCases = BBusAPIUseCases(networkService: NetworkService(), + persistenceStorage: PersistenceStorage(), + tokenManageType: TokenManager.self, + requestFactory: RequestFactory()) + let apiUseCase = StationAPIUseCase(useCases: apiUseCases) + let calculateUseCase = StationCalculateUseCase() + let viewModel = StationViewModel(apiUseCase: apiUseCase, + calculateUseCase: calculateUseCase, + arsId: arsId) let viewController = StationViewController(viewModel: viewModel) viewController.coordinator = self self.navigationPresenter.pushViewController(viewController, animated: true) diff --git a/BBus/BBus/Foreground/Station/StationViewController.swift b/BBus/BBus/Foreground/Station/StationViewController.swift index 8e44b229..75ccb172 100644 --- a/BBus/BBus/Foreground/Station/StationViewController.swift +++ b/BBus/BBus/Foreground/Station/StationViewController.swift @@ -8,45 +8,20 @@ import UIKit import Combine -final class StationViewController: UIViewController { +final class StationViewController: UIViewController, BaseViewControllerType { - @Published private var stationBusInfoHeight: CGFloat? + weak var coordinator: StationCoordinator? + private let viewModel: StationViewModel? + private lazy var stationView = StationView() + + private var cancellables: Set = [] + private var collectionHeightConstraint: NSLayoutConstraint? private var collectionViewMinHeight: CGFloat { let twice: CGFloat = 2 return self.view.frame.height - (StationHeaderView.headerHeight*twice) } - weak var coordinator: StationCoordinator? - private let viewModel: StationViewModel? - private lazy var customNavigationBar: CustomNavigationBar = { - let bar = CustomNavigationBar() - bar.configureTintColor(color: BBusColor.white) - if let bbusGray = BBusColor.bbusGray { - bar.configureBackgroundColor(color: bbusGray) - } - bar.configureAlpha(alpha: 0) - return bar - }() - private lazy var stationView: StationView = { - let view = StationView() - view.backgroundColor = BBusColor.white - return view - }() - private lazy var refreshButton: ThrottleButton = { - let radius: CGFloat = 25 - - let button = ThrottleButton() - button.setImage(BBusImage.refresh, for: .normal) - button.layer.cornerRadius = radius - button.tintColor = UIColor.white - button.backgroundColor = UIColor.darkGray - button.addTouchUpEventWithThrottle(delay: ThrottleButton.refreshInterval) { [weak self] in - self?.viewModel?.refresh() - } - return button - }() - private var collectionHeightConstraint: NSLayoutConstraint? - private var cancellables: Set = [] + @Published private var stationBusInfoHeight: CGFloat? init(viewModel: StationViewModel) { self.viewModel = viewModel @@ -60,18 +35,16 @@ final class StationViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - - self.binding() + self.baseViewDidLoad() self.configureColor() - self.configureLayout() - self.configureDelegate() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + self.baseViewWillAppear() + self.stationView.startLoader() self.viewModel?.configureObserver() - self.viewModel?.refresh() } override func viewWillDisappear(_ animated: Bool) { @@ -79,62 +52,62 @@ final class StationViewController: UIViewController { self.viewModel?.cancelObserver() } - // MARK: - Configure - private func configureLayout() { - let refreshButtonWidthAnchor: CGFloat = 50 - let refreshTrailingBottomInterval: CGFloat = -16 - - self.view.addSubviews(self.stationView, self.customNavigationBar, self.refreshButton) + // MARK: - Configuration + func configureLayout() { + self.view.addSubviews(self.stationView) NSLayoutConstraint.activate([ self.stationView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), self.stationView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), self.stationView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), self.stationView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) - ]) + ]) - NSLayoutConstraint.activate([ - self.customNavigationBar.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), - self.customNavigationBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), - self.customNavigationBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) - ]) - self.stationBusInfoHeight = nil + } - NSLayoutConstraint.activate([ - self.refreshButton.widthAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), - self.refreshButton.heightAnchor.constraint(equalToConstant: refreshButtonWidthAnchor), - self.refreshButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: refreshTrailingBottomInterval), - self.refreshButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: refreshTrailingBottomInterval) - ]) + private func configureColor() { + self.view.backgroundColor = BBusColor.bbusGray + self.stationView.navigationBar.configureBackgroundColor(color: BBusColor.bbusGray) } - private func configureDelegate() { + func configureDelegate() { self.stationView.configureDelegate(self) - self.customNavigationBar.configureDelegate(self) } - private func binding() { + func refresh() { + self.viewModel?.refresh() + } + + func bindAll() { + self.bindStationBusInfoHeight() + self.bindStationInfo() + self.bindNextStation() + self.bindError() + self.bindStopLoader() + self.bindBusKeys() + } + + private func bindStationBusInfoHeight() { self.$stationBusInfoHeight .receive(on: DispatchQueue.main) .sink() { [weak self] height in self?.collectionHeightConstraint?.isActive = false self?.collectionHeightConstraint = self?.stationView.configureTableViewHeight(height: height) }.store(in: &self.cancellables) - - self.viewModel?.usecase.$stationInfo + } + + private func bindStationInfo() { + self.viewModel?.$stationInfo .receive(on: DispatchQueue.main) - .dropFirst() + .compactMap { $0 } .sink(receiveValue: { [weak self] station in - if let station = station { - self?.stationView.configureHeaderView(stationId: station.arsID, stationName: station.stationName) - } - else { - self?.noInfoAlert() - } + self?.stationView.configureHeaderView(stationId: station.arsID, stationName: station.stationName) }) .store(in: &self.cancellables) - + } + + private func bindNextStation() { self.viewModel?.$nextStation .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] nextStation in @@ -142,37 +115,27 @@ final class StationViewController: UIViewController { self?.stationView.configureNextStation(direction: nextStation) }) .store(in: &self.cancellables) - - self.viewModel?.$busKeys - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] _ in - guard let viewModel = self?.viewModel else { return } - - self?.stationView.reload() - - if viewModel.stopLoader { - self?.stationView.stopLoader() - } - }) - .store(in: &self.cancellables) - - self.viewModel?.$favoriteItems - .receive(on: DispatchQueue.main) - .compactMap { $0 } - .first() - .sink(receiveValue: { [weak self] _ in - self?.stationView.reload() - }) - .store(in: &self.cancellables) - - self.viewModel?.usecase.$networkError + } + + private func bindError() { + self.viewModel?.$error .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] error in - guard let _ = error else { return } - self?.networkAlert() + guard let error = error as? BBusAPIError else { return } + + switch error { + case .invalidStationError: + self?.noInfoAlert() + case .noneResultError: + self?.noneResultAlert() + default: + self?.networkAlert() + } }) .store(in: &self.cancellables) - + } + + private func bindStopLoader() { self.viewModel?.$stopLoader .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] isStop in @@ -183,20 +146,27 @@ final class StationViewController: UIViewController { .store(in: &self.cancellables) } + private func bindBusKeys() { + guard let viewModel = viewModel else { return } + viewModel.$busKeys + .combineLatest(viewModel.$stationInfo.compactMap{$0}, viewModel.$favoriteItems.compactMap{$0}.first()) + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] results in + self?.stationView.reload() + }) + .store(in: &self.cancellables) + } + private func networkAlert() { let controller = UIAlertController(title: "네트워크 장애", message: "네트워크 장애가 발생하여 앱이 정상적으로 동작되지 않습니다.", preferredStyle: .alert) let action = UIAlertAction(title: "확인", style: .default, handler: nil) controller.addAction(action) self.coordinator?.delegate?.presentAlertToNavigation(controller: controller, completion: nil) } - - private func configureColor() { - self.view.backgroundColor = BBusColor.bbusGray - } private func noInfoAlert() { let controller = UIAlertController(title: "정거장 에러", - message: "죄송합니다. 현재 정보가 제공되지 않는 정거장입니다.", + message: "서울 외 지역은 정거장 정보를 제공하지 않습니다. 죄송합니다", preferredStyle: .alert) let action = UIAlertAction(title: "확인", style: .default, @@ -204,6 +174,18 @@ final class StationViewController: UIViewController { controller.addAction(action) self.coordinator?.delegate?.presentAlertToNavigation(controller: controller, completion: nil) } + + private func noneResultAlert() { + let controller = UIAlertController(title: "정거장 에러", + message: "서울 외 지역의 버스만 정차하는 정거장은 정보를 제공하지 않습니다", + preferredStyle: .alert) + let action = UIAlertAction(title: "확인", + style: .default, + handler: { [weak self] _ in self?.coordinator?.terminate() }) + controller.addAction(action) + self.coordinator?.delegate?.presentAlertToNavigation(controller: controller, completion: nil) + } + } // MARK: - Delegate : CollectionView @@ -251,13 +233,14 @@ extension StationViewController: UICollectionViewDataSource { } // configure delegate and button - if let item = self.makeFavoriteItem(at: indexPath) { + if let item = self.makeFavoriteItem(at: indexPath), + let favoriteItems = viewModel.favoriteItems { cell.configure(delegate: self) - cell.configureButton(status: viewModel.favoriteItems.contains(item)) - // 즐겨찾기 버튼 터치 시에도 reload 대신 버튼 색상만 다시 configure하도록 바인딩 + cell.configureButton(status: favoriteItems.contains(item)) self.viewModel?.$favoriteItems .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak cell] favoriteItems in + guard let favoriteItems = favoriteItems else { return } cell?.configureButton(status: favoriteItems.contains(item)) }) .store(in: &cell.cancellables) @@ -337,13 +320,12 @@ extension StationViewController: UICollectionViewDelegateFlowLayout { // MARK: - Delegate : UIScrollView extension StationViewController: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { -// self.customNavigationBar.configureAlpha(alpha: CGFloat(scrollView.contentOffset.y/127)) let baseLineContentOffset = StationHeaderView.headerHeight - CustomNavigationBar.height if scrollView.contentOffset.y >= baseLineContentOffset { - self.customNavigationBar.configureAlpha(alpha: 1) + self.stationView.configureNavigationAlpha(alpha: 1) } else { - self.customNavigationBar.configureAlpha(alpha: CGFloat(scrollView.contentOffset.y/baseLineContentOffset)) + self.stationView.configureNavigationAlpha(alpha: CGFloat(scrollView.contentOffset.y/baseLineContentOffset)) } } @@ -372,6 +354,13 @@ extension StationViewController: BackButtonDelegate { } } +// MARK: - Delegate: RefreshButton +extension StationViewController: RefreshButtonDelegate { + func buttonTapped() { + self.viewModel?.refresh() + } +} + // MARK: - Delegate: LikeButton extension StationViewController: LikeButtonDelegate { func likeStationBus(at cell: UICollectionViewCell) { @@ -392,7 +381,7 @@ extension StationViewController: LikeButtonDelegate { private func makeFavoriteItem(at indexPath: IndexPath) -> FavoriteItemDTO? { guard let viewModel = self.viewModel, - let station = viewModel.usecase.stationInfo, + let station = viewModel.stationInfo, let key = viewModel.busKeys[indexPath.section] else { return nil } let item: FavoriteItemDTO if viewModel.activeBuses.count - 1 >= indexPath.section { @@ -412,7 +401,7 @@ extension StationViewController: AlarmButtonDelegate { func shouldGoToAlarmSettingScene(at cell: UICollectionViewCell) { guard let indexPath = self.indexPath(for: cell), let viewModel = viewModel, - let stationID = viewModel.usecase.stationInfo?.stationID, + let stationID = viewModel.stationInfo?.stationID, let key = viewModel.busKeys[indexPath.section] else { return } let bus: BusArriveInfo if viewModel.activeBuses.count - 1 >= indexPath.section { diff --git a/BBus/BBus/Foreground/Station/UseCase/StationAPIUsable.swift b/BBus/BBus/Foreground/Station/UseCase/StationAPIUsable.swift new file mode 100644 index 00000000..3a2f0103 --- /dev/null +++ b/BBus/BBus/Foreground/Station/UseCase/StationAPIUsable.swift @@ -0,0 +1,18 @@ +// +// StationAPIUsable.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation +import Combine + +protocol StationAPIUsable: BaseUseCase { + func loadStationList() -> AnyPublisher<[StationDTO], Error> + func refreshInfo(about arsId: String) -> AnyPublisher<[StationByUidItemDTO], Error> + func add(favoriteItem: FavoriteItemDTO) -> AnyPublisher + func remove(favoriteItem: FavoriteItemDTO) -> AnyPublisher + func getFavoriteItems() -> AnyPublisher<[FavoriteItemDTO], Error> + func loadRoute() -> AnyPublisher<[BusRouteDTO], Error> +} diff --git a/BBus/BBus/Foreground/Station/UseCase/StationAPIUseCase.swift b/BBus/BBus/Foreground/Station/UseCase/StationAPIUseCase.swift new file mode 100644 index 00000000..8ed97c5e --- /dev/null +++ b/BBus/BBus/Foreground/Station/UseCase/StationAPIUseCase.swift @@ -0,0 +1,58 @@ +// +// StationAPIUseCase.swift +// BBus +// +// Created by 김태훈 on 2021/11/01. +// + +import Foundation +import Combine + +final class StationAPIUseCase: StationAPIUsable { + typealias StationUseCases = GetStationByUidItemUsable & GetStationListUsable & CreateFavoriteItemUsable & DeleteFavoriteItemUsable & GetFavoriteItemListUsable & GetRouteListUsable + + private let useCases: StationUseCases + private var cancellables: Set + + init(useCases: StationUseCases) { + self.useCases = useCases + self.cancellables = [] + } + + func loadStationList() -> AnyPublisher<[StationDTO], Error> { + self.useCases.getStationList() + .decode(type: [StationDTO].self, decoder: JSONDecoder()) + .eraseToAnyPublisher() + } + + func refreshInfo(about arsId: String) -> AnyPublisher<[StationByUidItemDTO], Error> { + return self.useCases.getStationByUidItem(arsId: arsId) + .decode(type: StationByUidItemResult.self, decoder: JSONDecoder()) + .tryMap({ item in + item.msgBody.itemList + }) + .eraseToAnyPublisher() + } + + func add(favoriteItem: FavoriteItemDTO) -> AnyPublisher { + return self.useCases.createFavoriteItem(param: favoriteItem) + .eraseToAnyPublisher() + } + + func remove(favoriteItem: FavoriteItemDTO) -> AnyPublisher { + return self.useCases.deleteFavoriteItem(param: favoriteItem) + .eraseToAnyPublisher() + } + + func getFavoriteItems() -> AnyPublisher<[FavoriteItemDTO], Error> { + return self.useCases.getFavoriteItemList() + .decode(type: [FavoriteItemDTO].self, decoder: PropertyListDecoder()) + .eraseToAnyPublisher() + } + + func loadRoute() -> AnyPublisher<[BusRouteDTO], Error> { + return self.useCases.getRouteList() + .decode(type: [BusRouteDTO].self, decoder: JSONDecoder()) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Foreground/Station/UseCase/StationCalculatable.swift b/BBus/BBus/Foreground/Station/UseCase/StationCalculatable.swift new file mode 100644 index 00000000..f320efdb --- /dev/null +++ b/BBus/BBus/Foreground/Station/UseCase/StationCalculatable.swift @@ -0,0 +1,12 @@ +// +// StationCalculatable.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation + +protocol StationCalculatable: BaseUseCase { + func findStation(in stations: [StationDTO], with arsId: String) -> StationDTO? +} diff --git a/BBus/BBus/Foreground/Station/UseCase/StationCalculateUseCase.swift b/BBus/BBus/Foreground/Station/UseCase/StationCalculateUseCase.swift new file mode 100644 index 00000000..d22baf47 --- /dev/null +++ b/BBus/BBus/Foreground/Station/UseCase/StationCalculateUseCase.swift @@ -0,0 +1,15 @@ +// +// StationCalculateUseCase.swift +// BBus +// +// Created by 최수정 on 2021/11/30. +// + +import Foundation + +final class StationCalculateUseCase: StationCalculatable { + func findStation(in stations: [StationDTO], with arsId: String) -> StationDTO? { + let station = stations.filter() { $0.arsID == arsId } + return station.first + } +} diff --git a/BBus/BBus/Foreground/Station/UseCase/StationUseCase.swift b/BBus/BBus/Foreground/Station/UseCase/StationUseCase.swift deleted file mode 100644 index be801f69..00000000 --- a/BBus/BBus/Foreground/Station/UseCase/StationUseCase.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// StationUseCase.swift -// BBus -// -// Created by 김태훈 on 2021/11/01. -// - -import Foundation -import Combine - -final class StationUsecase { - static let queue = DispatchQueue.init(label: "station") - - typealias StationUsecases = GetStationByUidItemUsecase & GetStationListUsecase & CreateFavoriteItemUsecase & DeleteFavoriteItemUsecase & GetFavoriteItemListUsecase & GetRouteListUsecase - - private let usecases: StationUsecases - @Published private(set) var busRouteList: [BusRouteDTO] - @Published private(set) var busArriveInfo: [StationByUidItemDTO] - @Published private(set) var stationInfo: StationDTO? - @Published private(set) var favoriteItems: [FavoriteItemDTO] // need more - @Published private(set) var networkError: Error? - private var cancellables: Set - - init(usecases: StationUsecases) { - self.usecases = usecases - self.busRouteList = [] - self.busArriveInfo = [] - self.stationInfo = nil - self.cancellables = [] - self.favoriteItems = [] - self.networkError = nil - - self.startStation() - } - - func startStation() { - self.loadRoute() - self.getFavoriteItems() - } - - func stationInfoWillLoad(with arsId: String) { - Self.queue.async { - self.usecases.getStationList() - .receive(on: Self.queue) - .decode(type: [StationDTO].self, decoder: JSONDecoder()) - .tryMap({ [weak self] stations in - return self?.findStation(in: stations, with: arsId) - }) - .retry({ [weak self] in - self?.stationInfoWillLoad(with: arsId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$stationInfo) - } - } - - private func findStation(in stations: [StationDTO], with arsId: String) -> StationDTO? { - let station = stations.filter() { $0.arsID == arsId } - return station.first - } - - func refreshInfo(about arsId: String) { - Self.queue.async { - self.usecases.getStationByUidItem(arsId: arsId) - .receive(on: Self.queue) - .decode(type: StationByUidItemResult.self, decoder: JSONDecoder()) - .tryMap({ item in - item.msgBody.itemList - }) - .retry({ [weak self] in - self?.refreshInfo(about: arsId) - }, handler: { [weak self] error in - self?.networkError = error - }) - .combineLatest(self.$busRouteList) { (busRouteList, entireBusRouteList) in - busRouteList.filter { busRoute in - entireBusRouteList.contains{ $0.routeID == busRoute.busRouteId } - } - } - .assign(to: &self.$busArriveInfo) - } - } - - func add(favoriteItem: FavoriteItemDTO) { - Self.queue.async { - self.usecases.createFavoriteItem(param: favoriteItem) - .receive(on: Self.queue) - .retry({ [weak self] in - self?.add(favoriteItem: favoriteItem) - }, handler: { [weak self] error in - self?.networkError = error - }) - .sink(receiveValue: { [weak self] _ in - self?.getFavoriteItems() - }) - .store(in: &self.cancellables) - } - } - - func remove(favoriteItem: FavoriteItemDTO) { - Self.queue.async { - self.usecases.deleteFavoriteItem(param: favoriteItem) - .receive(on: Self.queue) - .retry({ [weak self] in - self?.remove(favoriteItem: favoriteItem) - }, handler: { [weak self] error in - self?.networkError = error - }) - .sink(receiveValue: { [weak self] _ in - self?.getFavoriteItems() - }) - .store(in: &self.cancellables) - } - } - - private func getFavoriteItems() { - Self.queue.async { - self.usecases.getFavoriteItemList() - .receive(on: Self.queue) - .decode(type: [FavoriteItemDTO].self, decoder: PropertyListDecoder()) - .retry({ [weak self] in - self?.getFavoriteItems() - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$favoriteItems) - } - } - - private func loadRoute() { - Self.queue.async { - self.usecases.getRouteList() - .receive(on: Self.queue) - .decode(type: [BusRouteDTO].self, decoder: JSONDecoder()) - .retry({ [weak self] in - self?.loadRoute() - }, handler: { [weak self] error in - self?.networkError = error - }) - .assign(to: &self.$busRouteList) - } - } -} diff --git a/BBus/BBus/Foreground/Station/View/StationView.swift b/BBus/BBus/Foreground/Station/View/StationView.swift index 59a6710a..86531e5c 100644 --- a/BBus/BBus/Foreground/Station/View/StationView.swift +++ b/BBus/BBus/Foreground/Station/View/StationView.swift @@ -7,7 +7,7 @@ import UIKit -final class StationView: UIView { +final class StationView: NavigatableView { private lazy var colorBackgroundView: UIView = { let view = UIView() @@ -35,17 +35,23 @@ final class StationView: UIView { }() private lazy var loader: UIActivityIndicatorView = { let loader = UIActivityIndicatorView(style: .large) + loader.color = BBusColor.gray return loader }() convenience init() { self.init(frame: CGRect()) + self.configureColor() self.configureLayout() } // MARK: - Configure - private func configureLayout() { + private func configureColor() { + self.backgroundColor = BBusColor.white + } + + override func configureLayout() { let half: CGFloat = 0.5 self.addSubviews(self.colorBackgroundView, self.stationScrollView, self.loader) @@ -94,12 +100,16 @@ final class StationView: UIView { self.loader.centerXAnchor.constraint(equalTo: self.centerXAnchor), self.loader.centerYAnchor.constraint(equalTo: self.centerYAnchor) ]) + + super.configureLayout() } - func configureDelegate(_ delegate: UICollectionViewDelegate & UICollectionViewDataSource & UIScrollViewDelegate) { + func configureDelegate(_ delegate: UICollectionViewDelegate & UICollectionViewDataSource & UIScrollViewDelegate & BackButtonDelegate & RefreshButtonDelegate) { self.stationBodyCollectionView.delegate = delegate self.stationBodyCollectionView.dataSource = delegate self.stationScrollView.delegate = delegate + self.refreshButton.configureDelegate(delegate) + self.navigationBar.configureDelegate(delegate) } func configureTableViewHeight(height: CGFloat?) -> NSLayoutConstraint { @@ -143,4 +153,8 @@ final class StationView: UIView { self.loader.isHidden = true self.loader.stopAnimating() } + + func configureNavigationAlpha(alpha: CGFloat) { + self.navigationBar.configureAlpha(alpha: alpha) + } } diff --git a/BBus/BBus/Foreground/Station/ViewModel/StationViewModel.swift b/BBus/BBus/Foreground/Station/ViewModel/StationViewModel.swift index 3a048e83..94241fa4 100644 --- a/BBus/BBus/Foreground/Station/ViewModel/StationViewModel.swift +++ b/BBus/BBus/Foreground/Station/ViewModel/StationViewModel.swift @@ -7,27 +7,38 @@ import Foundation import Combine -import UIKit final class StationViewModel { - let usecase: StationUsecase + let apiUseCase: StationAPIUsable + let calculateUseCase: StationCalculatable let arsId: String - private var cancellables: Set + @Published private(set) var stationInfo: StationDTO? + @Published private(set) var busRouteList: [BusRouteDTO] @Published private(set) var busKeys: BusSectionKeys - @Published private(set) var activeBuses = [BBusRouteType: BusArriveInfos]() - private(set) var inActiveBuses = [BBusRouteType: BusArriveInfos]() - @Published private(set) var favoriteItems = [FavoriteItemDTO]() - @Published private(set) var nextStation: String? = nil - @Published private(set) var stopLoader: Bool = false + @Published private(set) var activeBuses: [BBusRouteType: BusArriveInfos] + private(set) var inActiveBuses: [BBusRouteType: BusArriveInfos] + @Published private(set) var favoriteItems: [FavoriteItemDTO]? + @Published private(set) var nextStation: String? + @Published private(set) var stopLoader: Bool + @Published private(set) var error: Error? + private var cancellables: Set - init(usecase: StationUsecase, arsId: String) { - self.usecase = usecase + init(apiUseCase: StationAPIUsable, calculateUseCase: StationCalculatable, arsId: String) { + self.apiUseCase = apiUseCase + self.calculateUseCase = calculateUseCase self.arsId = arsId - self.cancellables = [] + self.busRouteList = [] + self.stationInfo = nil self.busKeys = BusSectionKeys() - self.binding() - self.refresh() + self.favoriteItems = nil + self.nextStation = nil + self.activeBuses = [:] + self.inActiveBuses = [:] + self.cancellables = [] + self.stopLoader = false + + self.bind() } func configureObserver() { @@ -40,8 +51,28 @@ final class StationViewModel { } @objc func refresh() { - self.usecase.stationInfoWillLoad(with: arsId) - self.usecase.refreshInfo(about: arsId) + self.apiUseCase.refreshInfo(about: self.arsId) + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + self?.error = error + }) + .combineLatest(self.$busRouteList.filter { !$0.isEmpty }) { (busRouteList, entireBusRouteList) in + return busRouteList.filter { busRoute in + entireBusRouteList.contains{ $0.routeID == busRoute.busRouteId } + } + } + .tryMap({ arriveInfo -> [StationByUidItemDTO] in + guard arriveInfo.count > 0 else { throw BBusAPIError.noneResultError } + return arriveInfo + }) + .catchError({ [weak self] error in + self?.error = error + }) + .sink(receiveValue: { [weak self] arriveInfo in + self?.nextStation = arriveInfo.first?.nextStation + self?.classifyByRouteType(with: arriveInfo) + }) + .store(in: &self.cancellables) } @objc private func descendTime() { @@ -50,32 +81,48 @@ final class StationViewModel { }) } - private func binding() { + private func bind() { self.bindLoader() + self.bindStationInfo(with: self.arsId) + self.bindBusRouteList() self.bindFavoriteItems() - self.bindBusArriveInfo() } - private func bindBusArriveInfo() { - self.usecase.$busArriveInfo - .receive(on: StationUsecase.queue) - .sink(receiveCompletion: { error in - print(error) - }, receiveValue: { [weak self] arriveInfo in - guard arriveInfo.count > 0 else { return } - self?.nextStation = arriveInfo[0].nextStation - self?.classifyByRouteType(with: arriveInfo) + private func bindStationInfo(with arsId: String) { + self.apiUseCase.loadStationList() + .map({ [weak self] stations in + return self?.calculateUseCase.findStation(in: stations, with: arsId) }) - .store(in: &self.cancellables) + .tryMap({ stationInfo in + guard let stationInfo = stationInfo else { + throw BBusAPIError.invalidStationError + } + return stationInfo + }) + .catchError({ [weak self] error in + self?.error = error + }) + .assign(to: &self.$stationInfo) + } + + private func bindBusRouteList() { + self.apiUseCase.loadRoute() + .catchError({ [weak self] error in + self?.error = error + }) + .assign(to: &self.$busRouteList) } private func bindFavoriteItems() { - self.usecase.$favoriteItems - .receive(on: StationUsecase.queue) - .sink(receiveValue: { [weak self] items in - self?.favoriteItems = items.filter() { $0.arsId == self?.arsId } + self.apiUseCase.getFavoriteItems() + .receive(on: DispatchQueue.global()) + .catchError({ [weak self] error in + self?.error = error }) - .store(in: &self.cancellables) + .map({ [weak self] items -> [FavoriteItemDTO] in + return items.filter({ $0.arsId == self?.arsId }) + }) + .assign(to: &self.$favoriteItems) } private func classifyByRouteType(with buses: [StationByUidItemDTO]) { @@ -124,18 +171,52 @@ final class StationViewModel { } func add(favoriteItem: FavoriteItemDTO) { - self.usecase.add(favoriteItem: favoriteItem) + self.apiUseCase.add(favoriteItem: favoriteItem) + .catchError({ [weak self] error in + self?.error = error + }) + .sink { [weak self] _ in + guard let self = self else { return } + self.apiUseCase.getFavoriteItems() + .catchError({ [weak self] error in + self?.error = error + }) + .compactMap { $0 } + .assign(to: &self.$favoriteItems) + } + .store(in: &self.cancellables) } func remove(favoriteItem: FavoriteItemDTO) { - self.usecase.remove(favoriteItem: favoriteItem) + self.apiUseCase.remove(favoriteItem: favoriteItem) + .catchError({ [weak self] error in + self?.error = error + }) + .sink { [weak self] _ in + guard let self = self else { return } + self.apiUseCase.getFavoriteItems() + .catchError({ [weak self] error in + self?.error = error + }) + .compactMap { $0 } + .assign(to: &self.$favoriteItems) + } + .store(in: &self.cancellables) } private func bindLoader() { - self.$busKeys.zip(self.$favoriteItems, self.$nextStation) - .dropFirst() - .sink(receiveValue: { result in - self.stopLoader = true + self.$busKeys + .zip(self.$favoriteItems, self.$stationInfo) + .output(at: 1) + .sink(receiveValue: { [weak self] _ in + self?.stopLoader = true + }) + .store(in: &self.cancellables) + + self.$busKeys + .dropFirst(2) + .sink(receiveValue: { [weak self] result in + self?.stopLoader = true }) .store(in: &self.cancellables) } diff --git a/BBus/BBus/Global/Base/BaseUseCase.swift b/BBus/BBus/Global/Base/BaseUseCase.swift new file mode 100644 index 00000000..37d2e45f --- /dev/null +++ b/BBus/BBus/Global/Base/BaseUseCase.swift @@ -0,0 +1,10 @@ +// +// BaseUseCase.swift +// BBus +// +// Created by 김태훈 on 2021/11/29. +// + +import Foundation + +protocol BaseUseCase { } diff --git a/BBus/BBus/Global/Base/BaseViewController.swift b/BBus/BBus/Global/Base/BaseViewController.swift new file mode 100644 index 00000000..5e17e90c --- /dev/null +++ b/BBus/BBus/Global/Base/BaseViewController.swift @@ -0,0 +1,33 @@ +// +// BaseViewController.swift +// BBus +// +// Created by 김태훈 on 2021/11/29. +// + +import UIKit + +protocol BaseViewControllerType: UIViewController { + + func viewDidLoad() + + // MARK: configure + func configureLayout() + func configureDelegate() + func refresh() + + // MARK: bind + func bindAll() +} + +extension BaseViewControllerType { + func baseViewDidLoad() { + self.configureLayout() + self.configureDelegate() + self.bindAll() + } + + func baseViewWillAppear() { + self.refresh() + } +} diff --git a/BBus/BBus/Global/Constant/BBusRouteType.swift b/BBus/BBus/Global/Constant/BBusRouteType.swift index 0c785907..318ed974 100644 --- a/BBus/BBus/Global/Constant/BBusRouteType.swift +++ b/BBus/BBus/Global/Constant/BBusRouteType.swift @@ -8,7 +8,7 @@ import Foundation enum BBusRouteType: Int { - case shared = 0, airport, town, gansun, jisun, circular, wideArea, incheon, gyeonggi, closed + case shared = 0, airport, town, gansun, jisun, circular, wideArea, incheon, gyeonggi, closed, lateNight func toString() -> String { let common = "버스" @@ -23,14 +23,15 @@ enum BBusRouteType: Int { case .incheon: return "인천" + common case .gyeonggi: return "경기" + common case .closed: return "폐지" + common + case .lateNight: return "심야" + common } } func toRouteType() -> RouteType? { switch self { case .shared: return RouteType.customized - case .airport: return RouteType.mainLine - case .town: return RouteType.customized + case .airport: return RouteType.airport + case .town: return RouteType.town case .gansun: return RouteType.mainLine case .jisun: return RouteType.localLine case .circular: return RouteType.circulation @@ -38,6 +39,7 @@ enum BBusRouteType: Int { case .incheon: return nil case .gyeonggi: return nil case .closed: return nil + case .lateNight: return RouteType.lateNight } } } diff --git a/BBus/BBus/Global/Coordinator/AppCoordinator.swift b/BBus/BBus/Global/Coordinator/AppCoordinator.swift index 3d49dc89..2f2cf260 100644 --- a/BBus/BBus/Global/Coordinator/AppCoordinator.swift +++ b/BBus/BBus/Global/Coordinator/AppCoordinator.swift @@ -14,6 +14,10 @@ final class AppCoordinator: NSObject, Coordinator { var navigationPresenter: UINavigationController var movingStatusPresenter: UIViewController? var childCoordinators: [Coordinator] + + var statusBarHeight: CGFloat? { + return self.navigationWindow.windowScene?.statusBarManager?.statusBarFrame.height + } init(navigationWindow: UIWindow, movingStatusWindow: UIWindow) { self.navigationWindow = navigationWindow @@ -33,7 +37,7 @@ final class AppCoordinator: NSObject, Coordinator { coordinator.delegate = self coordinator.navigationPresenter = self.navigationPresenter self.childCoordinators.append(coordinator) - coordinator.start() + coordinator.start(statusBarHeight: self.statusBarHeight) self.navigationWindow.makeKeyAndVisible() self.movingStatusWindow.makeKeyAndVisible() @@ -67,8 +71,17 @@ extension AppCoordinator: MovingStatusOpenCloseDelegate { self.navigationWindow.frame.size = CGSize(width: self.navigationWindow.frame.width, height: self.navigationWindow.frame.height - MovingStatusView.bottomIndicatorHeight) } - let usecase = MovingStatusUsecase(usecases: BBusAPIUsecases(on: MovingStatusUsecase.queue)) - let viewModel = MovingStatusViewModel(usecase: usecase, busRouteId: busRouteId, fromArsId: fromArsId, toArsId: toArsId) + let apiUseCases = BBusAPIUseCases(networkService: NetworkService(), + persistenceStorage: PersistenceStorage(), + tokenManageType: TokenManager.self, + requestFactory: RequestFactory()) + let apiUseCase = MovingStatusAPIUseCase(useCases: apiUseCases) + let calculateUseCase = MovingStatusCalculateUseCase() + let viewModel = MovingStatusViewModel(apiUseCase: apiUseCase, + calculateUseCase: calculateUseCase, + busRouteId: busRouteId, + fromArsId: fromArsId, + toArsId: toArsId) let viewController = MovingStatusViewController(viewModel: viewModel) viewController.coordinator = self self.movingStatusPresenter = viewController @@ -80,8 +93,17 @@ extension AppCoordinator: MovingStatusOpenCloseDelegate { } func reset(busRouteId: Int, fromArsId: String, toArsId: String) { - let usecase = MovingStatusUsecase(usecases: BBusAPIUsecases(on: MovingStatusUsecase.queue)) - let viewModel = MovingStatusViewModel(usecase: usecase, busRouteId: busRouteId, fromArsId: fromArsId, toArsId: toArsId) + let apiUseCases = BBusAPIUseCases(networkService: NetworkService(), + persistenceStorage: PersistenceStorage(), + tokenManageType: TokenManager.self, + requestFactory: RequestFactory()) + let apiUseCase = MovingStatusAPIUseCase(useCases: apiUseCases) + let calculateUseCase = MovingStatusCalculateUseCase() + let viewModel = MovingStatusViewModel(apiUseCase: apiUseCase, + calculateUseCase: calculateUseCase, + busRouteId: busRouteId, + fromArsId: fromArsId, + toArsId: toArsId) let viewController = MovingStatusViewController(viewModel: viewModel) viewController.coordinator = self self.movingStatusPresenter = viewController @@ -173,4 +195,3 @@ extension AppCoordinator: CoordinatorFinishDelegate { } } } - diff --git a/BBus/BBus/Global/Coordinator/Coordinator.swift b/BBus/BBus/Global/Coordinator/Coordinator.swift index 7e07172f..b56fdb18 100644 --- a/BBus/BBus/Global/Coordinator/Coordinator.swift +++ b/BBus/BBus/Global/Coordinator/Coordinator.swift @@ -32,12 +32,9 @@ typealias CoordinatorDelegate = (CoordinatorFinishDelegate & CoordinatorCreateDe protocol Coordinator: AnyObject { var navigationPresenter: UINavigationController { get set } var delegate: CoordinatorDelegate? { get set } - func start() } extension Coordinator { - func start() { } - func coordinatorDidFinish() { self.delegate?.removeChildCoordinator(self) } diff --git a/BBus/BBus/Global/View/CustomNavigationBar.swift b/BBus/BBus/Global/CustomView/CustomNavigationBar.swift similarity index 94% rename from BBus/BBus/Global/View/CustomNavigationBar.swift rename to BBus/BBus/Global/CustomView/CustomNavigationBar.swift index 19cb6fe4..9e6fb8ed 100644 --- a/BBus/BBus/Global/View/CustomNavigationBar.swift +++ b/BBus/BBus/Global/CustomView/CustomNavigationBar.swift @@ -53,6 +53,7 @@ final class CustomNavigationBar: UIView { self.init(frame: CGRect()) self.configureLayout() + self.configureDefaultColor() } // MARK: - Configure @@ -89,7 +90,7 @@ final class CustomNavigationBar: UIView { } // MARK: - Configure NavigationBar - func configureTintColor(color: UIColor) { + func configureTintColor(color: UIColor?) { self.backButton.tintColor = color self.backButtonTitleLabel.textColor = color self.titleLabel.textColor = color @@ -135,4 +136,10 @@ final class CustomNavigationBar: UIView { range: range) self.titleLabel.attributedText = attributedString } + + private func configureDefaultColor() { + self.configureBackgroundColor(color: BBusColor.white) + self.configureTintColor(color: BBusColor.white) + self.configureAlpha(alpha: 0) + } } diff --git a/BBus/BBus/Global/CustomView/NavigatableView.swift b/BBus/BBus/Global/CustomView/NavigatableView.swift new file mode 100644 index 00000000..eabbb210 --- /dev/null +++ b/BBus/BBus/Global/CustomView/NavigatableView.swift @@ -0,0 +1,25 @@ +// +// NavigatableView.swift +// BBus +// +// Created by 김태훈 on 2021/11/30. +// + +import UIKit + +class NavigatableView: RefreshableView { + + lazy var navigationBar = CustomNavigationBar() + + override func configureLayout() { + self.addSubviews(self.navigationBar) + + NSLayoutConstraint.activate([ + self.navigationBar.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor), + self.navigationBar.leadingAnchor.constraint(equalTo: self.leadingAnchor), + self.navigationBar.trailingAnchor.constraint(equalTo: self.trailingAnchor) + ]) + + super.configureLayout() + } +} diff --git a/BBus/BBus/Global/CustomView/RefreshButton.swift b/BBus/BBus/Global/CustomView/RefreshButton.swift new file mode 100644 index 00000000..52c4a914 --- /dev/null +++ b/BBus/BBus/Global/CustomView/RefreshButton.swift @@ -0,0 +1,40 @@ +// +// RefreshButton.swift +// BBus +// +// Created by 김태훈 on 2021/11/29. +// + +import UIKit + +protocol RefreshButtonDelegate: AnyObject { + func buttonTapped() +} + +final class RefreshButton: ThrottleButton { + + static let refreshButtonWidth: CGFloat = 50 + private weak var delegate: RefreshButtonDelegate? { + didSet { + self.addTouchUpEventWithThrottle(delay: ThrottleButton.refreshInterval) { + self.delegate?.buttonTapped() + } + } + } + + convenience init() { + self.init(frame: CGRect()) + self.configureUI() + } + + private func configureUI() { + self.setImage(BBusImage.refresh, for: .normal) + self.layer.cornerRadius = Self.refreshButtonWidth / 2 + self.tintColor = BBusColor.white + self.backgroundColor = BBusColor.darkGray + } + + func configureDelegate(_ delegate: RefreshButtonDelegate) { + self.delegate = delegate + } +} diff --git a/BBus/BBus/Global/CustomView/RefreshableView.swift b/BBus/BBus/Global/CustomView/RefreshableView.swift new file mode 100644 index 00000000..a565278e --- /dev/null +++ b/BBus/BBus/Global/CustomView/RefreshableView.swift @@ -0,0 +1,25 @@ +// +// RefreshableView.swift +// BBus +// +// Created by 김태훈 on 2021/11/29. +// + +import UIKit + +class RefreshableView: UIView { + + lazy var refreshButton = RefreshButton() + + func configureLayout() { + self.addSubviews(self.refreshButton) + + let refreshTrailingBottomInterval: CGFloat = -16 + NSLayoutConstraint.activate([ + self.refreshButton.widthAnchor.constraint(equalToConstant: RefreshButton.refreshButtonWidth), + self.refreshButton.heightAnchor.constraint(equalTo: self.refreshButton.widthAnchor), + self.refreshButton.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: refreshTrailingBottomInterval), + self.refreshButton.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: refreshTrailingBottomInterval) + ]) + } +} diff --git a/BBus/BBus/Global/View/ThrottleButton.swift b/BBus/BBus/Global/CustomView/ThrottleButton.swift similarity index 96% rename from BBus/BBus/Global/View/ThrottleButton.swift rename to BBus/BBus/Global/CustomView/ThrottleButton.swift index 53d3c8a3..7b94e3c7 100644 --- a/BBus/BBus/Global/View/ThrottleButton.swift +++ b/BBus/BBus/Global/CustomView/ThrottleButton.swift @@ -7,7 +7,7 @@ import UIKit -final class ThrottleButton: UIButton { +class ThrottleButton: UIButton { static let refreshInterval: Double = 3 diff --git a/BBus/BBus/Global/DTO/BusPosByRtidDTO.swift b/BBus/BBus/Global/DTO/BusPosByRtidDTO.swift index 02f498ee..b19c81ff 100644 --- a/BBus/BBus/Global/DTO/BusPosByRtidDTO.swift +++ b/BBus/BBus/Global/DTO/BusPosByRtidDTO.swift @@ -49,4 +49,15 @@ struct BusPosByRtidDTO: Codable { self.gpsY = Double((try? container.decode(String.self, forKey: .gpsY)) ?? "") ?? 0 self.gpsX = Double((try? container.decode(String.self, forKey: .gpsX)) ?? "") ?? 0 } + + init(busType: Int, congestion: Int, plainNumber: String, sectionOrder: Int, fullSectDist: String, sectDist: String, gpsY: Double, gpsX: Double) { + self.busType = busType + self.congestion = congestion + self.plainNumber = plainNumber + self.sectionOrder = sectionOrder + self.fullSectDist = fullSectDist + self.sectDist = sectDist + self.gpsY = gpsY + self.gpsX = gpsX + } } diff --git a/BBus/BBus/Global/DTO/BusPosByVehicleIdDTO.swift b/BBus/BBus/Global/DTO/BusPosByVehicleIdDTO.swift index 3e19e4d0..13070ab1 100644 --- a/BBus/BBus/Global/DTO/BusPosByVehicleIdDTO.swift +++ b/BBus/BBus/Global/DTO/BusPosByVehicleIdDTO.swift @@ -7,30 +7,6 @@ import Foundation -struct JsonHeader: Codable { - let msgHeader: MessageHeader -} - -struct JsonMessage: Codable { - let msgHeader: MessageHeader - let msgBody: MessageBody -} - -struct MessageHeader: Codable { - let headerMessage, headerCD: String - let itemCount: Int - - enum CodingKeys: String, CodingKey { - case headerMessage = "headerMsg" - case headerCD = "headerCd" - case itemCount - } -} - -struct MessageBody: Codable { - let itemList: [T] -} - struct BusPosByVehicleIdDTO: Codable { let stationOrd: String diff --git a/BBus/BBus/Global/DTO/BusRouteDTO.swift b/BBus/BBus/Global/DTO/BusRouteDTO.swift index 9fa3aefa..b98dfacb 100644 --- a/BBus/BBus/Global/DTO/BusRouteDTO.swift +++ b/BBus/BBus/Global/DTO/BusRouteDTO.swift @@ -22,4 +22,6 @@ enum RouteType: String, Codable { case circulation = "순환" case lateNight = "심야" case localLine = "지선" + case airport = "공항" + case town = "마을" } diff --git a/BBus/BBus/Global/DTO/JsonDTO.swift b/BBus/BBus/Global/DTO/JsonDTO.swift new file mode 100644 index 00000000..3ff36f39 --- /dev/null +++ b/BBus/BBus/Global/DTO/JsonDTO.swift @@ -0,0 +1,32 @@ +// +// JsonDTO.swift +// BBus +// +// Created by 최수정 on 2021/12/01. +// + +import Foundation + +struct JsonHeader: Codable { + let msgHeader: MessageHeader +} + +struct JsonMessage: Codable { + let msgHeader: MessageHeader + let msgBody: MessageBody +} + +struct MessageHeader: Codable { + let headerMessage, headerCD: String + let itemCount: Int + + enum CodingKeys: String, CodingKey { + case headerMessage = "headerMsg" + case headerCD = "headerCd" + case itemCount + } +} + +struct MessageBody: Codable { + let itemList: [T] +} diff --git a/BBus/BBus/Global/DTO/StationByRouteListDTO.swift b/BBus/BBus/Global/DTO/StationByRouteListDTO.swift index 3b45bfe5..09b30194 100644 --- a/BBus/BBus/Global/DTO/StationByRouteListDTO.swift +++ b/BBus/BBus/Global/DTO/StationByRouteListDTO.swift @@ -49,4 +49,15 @@ struct StationByRouteListDTO: Codable { self.lastTm = (try? container.decode(String.self, forKey: .lastTm)) ?? "" self.transYn = (try? container.decode(String.self, forKey: .transYn)) ?? "" } + + init(sectionSpeed: Int, sequence: Int, stationName: String, fullSectionDistance: Int, arsId: String, beginTm: String, lastTm: String, transYn: String) { + self.sectionSpeed = sectionSpeed + self.sequence = sequence + self.stationName = stationName + self.fullSectionDistance = fullSectionDistance + self.arsId = arsId + self.beginTm = beginTm + self.lastTm = lastTm + self.transYn = transYn + } } diff --git a/BBus/BBus/Global/DeviceConfig/AlarmCenter.swift b/BBus/BBus/Global/DeviceConfig/AlarmCenter.swift new file mode 100644 index 00000000..1f7de4f1 --- /dev/null +++ b/BBus/BBus/Global/DeviceConfig/AlarmCenter.swift @@ -0,0 +1,74 @@ +// +// PermissionManager.swift +// BBus +// +// Created by 이지수 on 2021/11/30. +// + +import CoreLocation +import UserNotifications +import UIKit + +protocol AlarmManagable { + func configurePermission() + func pushAlarm(in identifier: String, title: String, message: String) +} + +protocol AlarmDetailConfigurable: AlarmManagable { + func configureLocationDetail(_: CLLocationManagerDelegate) +} + +final class AlarmCenter: AlarmDetailConfigurable { + + private lazy var locationManager: CLLocationManager = CLLocationManager() + private lazy var userNotificationCenter: UNUserNotificationCenter = UNUserNotificationCenter.current() + + init() {} + + func configurePermission() { + self.configureLocationManager() + self.configureUserNotificationCenter() + } + + func configureLocationDetail(_ delegate: CLLocationManagerDelegate) { + self.locationManager.desiredAccuracy = kCLLocationAccuracyBest + self.locationManager.delegate = delegate + } + + private func configureLocationManager() { + self.locationManager.requestAlwaysAuthorization() + self.locationManager.allowsBackgroundLocationUpdates = true + self.locationManager.startUpdatingLocation() + self.locationUnauthorizedHandler(self.locationManager.authorizationStatus) + } + + private func configureUserNotificationCenter() { + self.userNotificationCenter.requestAuthorization(options: [.alert, .sound, .badge], completionHandler: { [weak self] didAllow, error in + guard didAllow == false else { return } + self?.openSetting() + }) + } + + func pushAlarm(in identifier: String, title: String, message: String) { + let content = UNMutableNotificationContent() + content.title = title + content.body = message + content.badge = Int(truncating: content.badge ?? 0) + 1 as NSNumber + + let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil) + + self.userNotificationCenter.add(request, withCompletionHandler: nil) + } + + private func openSetting() { + guard let settingUrl = URL(string: UIApplication.openSettingsURLString) else { return } + DispatchQueue.main.async { + UIApplication.shared.open(settingUrl) + } + } + + private func locationUnauthorizedHandler(_ status: CLAuthorizationStatus) { + guard status == .denied else { return } + self.openSetting() + } +} diff --git a/BBus/BBus/Global/Extension/PublisherExtension.swift b/BBus/BBus/Global/Extension/PublisherExtension.swift index 43b34d95..0be2d752 100644 --- a/BBus/BBus/Global/Extension/PublisherExtension.swift +++ b/BBus/BBus/Global/Extension/PublisherExtension.swift @@ -8,15 +8,15 @@ import Foundation import Combine -extension Publisher where Output == (Data, Int), Failure == Error { - func mapJsonBBusAPIError() -> AnyPublisher { - self.tryMap({ data, order -> Data in +extension Publisher where Output == Data, Failure == Error { + func mapJsonBBusAPIError(with removeAccessKeyHandler: @escaping () -> Void ) -> AnyPublisher { + self.tryMap({ data -> Data in guard let json = try? JSONDecoder().decode(JsonHeader.self, from: data), let statusCode = Int(json.msgHeader.headerCD), let error = BBusAPIError(errorCode: statusCode) else { return data } switch error { case .noneAccessKeyError, .noneRegisteredKeyError, .suspendedKeyError, .exceededKeyError: - Service.shared.removeAccessKey(at: order) + removeAccessKeyHandler() default: break } @@ -43,5 +43,15 @@ extension Publisher where Failure == Error { return publisher.eraseToAnyPublisher() }).eraseToAnyPublisher() } + + func catchError(_ handler: @escaping (Error) -> Void) -> AnyPublisher { + self.catch({ error -> AnyPublisher in + handler(error) + let publisher = PassthroughSubject() + DispatchQueue.global().async { + publisher.send(completion: .finished) + } + return publisher.eraseToAnyPublisher() + }).eraseToAnyPublisher() + } } - diff --git a/BBus/BBus/Global/Network/BBusAPIError.swift b/BBus/BBus/Global/Network/BBusAPIError.swift index cac4e86b..8583daaa 100644 --- a/BBus/BBus/Global/Network/BBusAPIError.swift +++ b/BBus/BBus/Global/Network/BBusAPIError.swift @@ -9,7 +9,7 @@ import Foundation enum BBusAPIError: Error { case systemError, noneParamError, wrongParamError, noneResultError, noneAccessKeyError, noneRegisteredKeyError, suspendedKeyError, exceededKeyError, wrongRequestError, wrongRouteIdError, wrongStationError, noneBusArriveInfoError, wrongStartStationIdError, wrongEndStationIdError, preparingAPIError, wrongFormatError, noMoreAccessKeyError, - trafficExceed + trafficExceed, invalidStationError init?(errorCode: Int) { switch errorCode { diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/BBusAPIUseCases.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/BBusAPIUseCases.swift new file mode 100644 index 00000000..bf068cd1 --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/BBusAPIUseCases.swift @@ -0,0 +1,24 @@ +// +// BBusAPIUsecases.swift +// BBus +// +// Created by Kang Minsang on 2021/11/10. +// + +import Foundation +import Combine + +struct BBusAPIUseCases { + let tokenManageType: TokenManagable.Type + + let networkService: NetworkServiceProtocol + let persistenceStorage: PersistenceStorageProtocol + let requestFactory: Requestable + + init(networkService: NetworkServiceProtocol, persistenceStorage: PersistenceStorageProtocol, tokenManageType: TokenManagable.Type, requestFactory: Requestable) { + self.networkService = networkService + self.persistenceStorage = persistenceStorage + self.tokenManageType = tokenManageType + self.requestFactory = requestFactory + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/CreateFavoriteItemUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/CreateFavoriteItemUseCase.swift new file mode 100644 index 00000000..59037fed --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/CreateFavoriteItemUseCase.swift @@ -0,0 +1,23 @@ +// +// CreateFavoriteItemUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: CreateFavoriteItemUsable { + func createFavoriteItem(param: FavoriteItemDTO) -> AnyPublisher { + let fetcher: CreateFavoriteItemFetchable = PersistenceCreateFavoriteItemFetcher(persistenceStorage: self.persistenceStorage) + return fetcher + .fetch(param: param) + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch(param: param) + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/DeleteFavoriteItemUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/DeleteFavoriteItemUseCase.swift new file mode 100644 index 00000000..596ed756 --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/DeleteFavoriteItemUseCase.swift @@ -0,0 +1,23 @@ +// +// DeleteFavoriteItemUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: DeleteFavoriteItemUsable { + func deleteFavoriteItem(param: FavoriteItemDTO) -> AnyPublisher { + let fetcher: DeleteFavoriteItemFetchable = PersistenceDeleteFavoriteItemFetcher(persistenceStorage: self.persistenceStorage) + return fetcher + .fetch(param: param) + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch(param: param) + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/GetArrInfoByRouteListUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/GetArrInfoByRouteListUseCase.swift new file mode 100644 index 00000000..414ad53c --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/GetArrInfoByRouteListUseCase.swift @@ -0,0 +1,26 @@ +// +// GetArrInfoByRouteListUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: GetArrInfoByRouteListUsable { + func getArrInfoByRouteList(stId: String, busRouteId: String, ord: String) -> AnyPublisher { + let param = ["stId": stId, "busRouteId": busRouteId, "ord": ord, "resultType": "json"] + let fetcher: GetArrInfoByRouteListFetchable = ServiceGetArrInfoByRouteListFetcher(networkService: self.networkService, + tokenManager: self.tokenManageType.init(), + requestFactory: self.requestFactory) + return fetcher + .fetch(param: param) + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch(param: param) + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/GetBusPosByRtidUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/GetBusPosByRtidUseCase.swift new file mode 100644 index 00000000..769ba3a6 --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/GetBusPosByRtidUseCase.swift @@ -0,0 +1,26 @@ +// +// GetBusPosByRtidUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: GetBusPosByRtidUsable { + func getBusPosByRtid(busRoutedId: String) -> AnyPublisher { + let param = ["busRouteId": busRoutedId, "resultType": "json"] + let fetcher: GetBusPosByRtidFetchable = ServiceGetBusPosByRtidFetcher(networkService: self.networkService, + tokenManager: TokenManager(), + requestFactory: self.requestFactory) + return fetcher + .fetch(param: param) + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch(param: param) + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/GetBusPosByVehIdUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/GetBusPosByVehIdUseCase.swift new file mode 100644 index 00000000..fc320b82 --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/GetBusPosByVehIdUseCase.swift @@ -0,0 +1,26 @@ +// +// GetBusPosByVehIdUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: GetBusPosByVehIdUsable { + func getBusPosByVehId(_ vehId: String) -> AnyPublisher { + let param = ["vehId": vehId, "resultType": "json"] + let fetcher: GetBusPosByVehIdFetchable = ServiceGetBusPosByVehIdFetcher(networkService: self.networkService, + tokenManager: TokenManager(), + requestFactory: self.requestFactory) + return fetcher + .fetch(param: param) + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch(param: param) + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/GetFavoriteItemListUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/GetFavoriteItemListUseCase.swift new file mode 100644 index 00000000..77865220 --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/GetFavoriteItemListUseCase.swift @@ -0,0 +1,23 @@ +// +// GetFavoriteItemListUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: GetFavoriteItemListUsable { + func getFavoriteItemList() -> AnyPublisher { + let fetcher: GetFavoriteItemListFetchable = PersistenceGetFavoriteItemListFetcher(persistenceStorage: self.persistenceStorage) + return fetcher + .fetch() + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch() + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/GetRouteListUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/GetRouteListUseCase.swift new file mode 100644 index 00000000..7c14e1bd --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/GetRouteListUseCase.swift @@ -0,0 +1,23 @@ +// +// GetRouteListUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: GetRouteListUsable { + func getRouteList() -> AnyPublisher { + let fetcher: GetRouteListFetchable = PersistenceGetRouteListFetcher(persistenceStorage: self.persistenceStorage) + return fetcher + .fetch() + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch() + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/GetStationByUidItemUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/GetStationByUidItemUseCase.swift new file mode 100644 index 00000000..57c6f65a --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/GetStationByUidItemUseCase.swift @@ -0,0 +1,26 @@ +// +// GetStationByUidItemUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: GetStationByUidItemUsable { + func getStationByUidItem(arsId: String) -> AnyPublisher { + let param = ["arsId": arsId, "resultType": "json"] + let fetcher: GetStationByUidItemFetchable = ServiceGetStationByUidItemFetcher(networkService: self.networkService, + tokenManager: TokenManager(), + requestFactory: self.requestFactory) + return fetcher + .fetch(param: param) + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch(param: param) + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/GetStationListUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/GetStationListUseCase.swift new file mode 100644 index 00000000..470ff3be --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/GetStationListUseCase.swift @@ -0,0 +1,23 @@ +// +// GetStationListUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: GetStationListUsable { + func getStationList() -> AnyPublisher { + let fetcher: GetStationListFetchable = PersistenceGetStationListFetcher(persistenceStorage: self.persistenceStorage) + return fetcher + .fetch() + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch() + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUseCases/GetStationsByRouteListUseCase.swift b/BBus/BBus/Global/Network/BBusAPIUseCases/GetStationsByRouteListUseCase.swift new file mode 100644 index 00000000..fee69eda --- /dev/null +++ b/BBus/BBus/Global/Network/BBusAPIUseCases/GetStationsByRouteListUseCase.swift @@ -0,0 +1,26 @@ +// +// GetStationsByRouteListUseCase.swift +// BBus +// +// Created by Kang Minsang on 2021/12/01. +// + +import Foundation +import Combine + +extension BBusAPIUseCases: GetStationsByRouteListUsable { + func getStationsByRouteList(busRoutedId: String) -> AnyPublisher { + let param = ["busRouteId": busRoutedId, "resultType": "json"] + let fetcher: GetStationsByRouteListFetchable = ServiceGetStationsByRouteListFetcher(networkService: self.networkService, + tokenManager: TokenManager(), + requestFactory: self.requestFactory) + return fetcher + .fetch(param: param) + .tryCatch({ error -> AnyPublisher in + return fetcher + .fetch(param: param) + }) + .retry(TokenManager.maxTokenCount) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/BBusAPIUsecases.swift b/BBus/BBus/Global/Network/BBusAPIUsecases.swift deleted file mode 100644 index f4951492..00000000 --- a/BBus/BBus/Global/Network/BBusAPIUsecases.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// BBusAPIUsecases.swift -// BBus -// -// Created by Kang Minsang on 2021/11/10. -// - -import Foundation -import Combine - -final class BBusAPIUsecases: RequestUsecases { - - private let queue: DispatchQueue - - init(on queue: DispatchQueue) { - self.queue = queue - } - - func getArrInfoByRouteList(stId: String, busRouteId: String, ord: String) -> AnyPublisher { - let param = ["stId": stId, "busRouteId": busRouteId, "ord": ord, "resultType": "json"] - let fetcher: GetArrInfoByRouteListFetchable = ServiceGetArrInfoByRouteListFetcher() - return fetcher - .fetch(param: param, on: self.queue) - } - - func getStationsByRouteList(busRoutedId: String) -> AnyPublisher { - let param = ["busRouteId": busRoutedId, "resultType": "json"] - let fetcher: GetStationsByRouteListFetchable = ServiceGetStationsByRouteListFetcher() - return fetcher - .fetch(param: param, on: self.queue) - } - - func getBusPosByRtid(busRoutedId: String) -> AnyPublisher { - let param = ["busRouteId": busRoutedId, "resultType": "json"] - let fetcher: GetBusPosByRtidFetchable = ServiceGetBusPosByRtidFetcher() - return fetcher - .fetch(param: param, on: self.queue) - } - - func getStationByUidItem(arsId: String) -> AnyPublisher { - let param = ["arsId": arsId, "resultType": "json"] - let fetcher: GetStationByUidItemFetchable = ServiceGetStationByUidItemFetcher() - return fetcher - .fetch(param: param, on: self.queue) - } - - func getBusPosByVehId(_ vehId: String) -> AnyPublisher { - let param = ["vehId": vehId, "resultType": "json"] - let fetcher: GetBusPosByVehIdFetchable = ServiceGetBusPosByVehIdFetcher() - return fetcher - .fetch(param: param, on: self.queue) - } - - func getRouteList() -> AnyPublisher { - let fetcher: GetRouteListFetchable = PersistentGetRouteListFetcher() - return fetcher - .fetch(on: self.queue) - } - - func getStationList() -> AnyPublisher { - let fetcher: GetStationListFetchable = PersistentGetStationListFetcher() - return fetcher - .fetch(on: self.queue) - } - - func getFavoriteItemList() -> AnyPublisher { - let fetcher: GetFavoriteItemListFetchable = PersistentGetFavoriteItemListFetcher() - return fetcher - .fetch(on: self.queue) - } - - func createFavoriteItem(param: FavoriteItemDTO) -> AnyPublisher { - let fetcher: CreateFavoriteItemFetchable = PersistentCreateFavoriteItemFetcher() - return fetcher - .fetch(param: param, on: self.queue) - } - - func deleteFavoriteItem(param: FavoriteItemDTO) -> AnyPublisher { - let fetcher: DeleteFavoriteItemFetchable = PersistentDeleteFavoriteItemFetcher() - return fetcher - .fetch(param: param, on: self.queue) - } -} diff --git a/BBus/BBus/Global/Network/Fetcher/CreateFavoriteItemFetcher.swift b/BBus/BBus/Global/Network/Fetcher/CreateFavoriteItemFetcher.swift index 0bf7f57f..04099597 100644 --- a/BBus/BBus/Global/Network/Fetcher/CreateFavoriteItemFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/CreateFavoriteItemFetcher.swift @@ -9,11 +9,14 @@ import Foundation import Combine protocol CreateFavoriteItemFetchable { - func fetch(param: FavoriteItemDTO, on queue: DispatchQueue) -> AnyPublisher + func fetch(param: FavoriteItemDTO) -> AnyPublisher } -final class PersistentCreateFavoriteItemFetcher: CreateFavoriteItemFetchable { - func fetch(param: FavoriteItemDTO, on queue: DispatchQueue) -> AnyPublisher { - return Persistent.shared.create(key: "FavoriteItems", param: param, on: queue) +struct PersistenceCreateFavoriteItemFetcher: PersistenceFetchable, CreateFavoriteItemFetchable { + private(set) var persistenceStorage: PersistenceStorageProtocol + + func fetch(param: FavoriteItemDTO) -> AnyPublisher { + return self.persistenceStorage.create(key: "FavoriteItems", param: param) + .eraseToAnyPublisher() } } diff --git a/BBus/BBus/Global/Network/Fetcher/DeleteFavoriteItemFetchable.swift b/BBus/BBus/Global/Network/Fetcher/DeleteFavoriteItemFetchable.swift index b30f6c10..7f380546 100644 --- a/BBus/BBus/Global/Network/Fetcher/DeleteFavoriteItemFetchable.swift +++ b/BBus/BBus/Global/Network/Fetcher/DeleteFavoriteItemFetchable.swift @@ -8,12 +8,15 @@ import Foundation import Combine -protocol DeleteFavoriteItemFetchable { - func fetch(param: FavoriteItemDTO, on queue: DispatchQueue) -> AnyPublisher +protocol DeleteFavoriteItemFetchable: PersistenceFetchable { + func fetch(param: FavoriteItemDTO) -> AnyPublisher } -final class PersistentDeleteFavoriteItemFetcher: DeleteFavoriteItemFetchable { - func fetch(param: FavoriteItemDTO, on queue: DispatchQueue) -> AnyPublisher { - return Persistent.shared.delete(key: "FavoriteItems", param: param, on: queue) +struct PersistenceDeleteFavoriteItemFetcher: DeleteFavoriteItemFetchable { + private(set) var persistenceStorage: PersistenceStorageProtocol + + func fetch(param: FavoriteItemDTO) -> AnyPublisher { + return self.persistenceStorage.delete(key: "FavoriteItems", param: param) + .eraseToAnyPublisher() } } diff --git a/BBus/BBus/Global/Network/Fetcher/GetArrInfoByRouteListFetcher.swift b/BBus/BBus/Global/Network/Fetcher/GetArrInfoByRouteListFetcher.swift index 88af08fd..ae1814ab 100644 --- a/BBus/BBus/Global/Network/Fetcher/GetArrInfoByRouteListFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/GetArrInfoByRouteListFetcher.swift @@ -9,12 +9,16 @@ import Foundation import Combine protocol GetArrInfoByRouteListFetchable { - func fetch(param: [String: String], on queue: DispatchQueue) -> AnyPublisher + func fetch(param: [String: String]) -> AnyPublisher } -final class ServiceGetArrInfoByRouteListFetcher: GetArrInfoByRouteListFetchable { - func fetch(param: [String: String], on queue: DispatchQueue) -> AnyPublisher { +struct ServiceGetArrInfoByRouteListFetcher: ServiceFetchable, GetArrInfoByRouteListFetchable { + private(set) var networkService: NetworkServiceProtocol + private(set) var tokenManager: TokenManagable + private(set) var requestFactory: Requestable + + func fetch(param: [String: String]) -> AnyPublisher { let url = "http://ws.bus.go.kr/api/rest/arrive/getArrInfoByRoute" - return Service.shared.get(url: url, params: param, on: queue).mapJsonBBusAPIError() + return self.fetch(url: url, param: param) } } diff --git a/BBus/BBus/Global/Network/Fetcher/GetBusPosByRtidFetcher.swift b/BBus/BBus/Global/Network/Fetcher/GetBusPosByRtidFetcher.swift index 2f42847a..f05ae528 100644 --- a/BBus/BBus/Global/Network/Fetcher/GetBusPosByRtidFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/GetBusPosByRtidFetcher.swift @@ -9,12 +9,16 @@ import Foundation import Combine protocol GetBusPosByRtidFetchable { - func fetch(param: [String: String], on queue: DispatchQueue) -> AnyPublisher + func fetch(param: [String: String]) -> AnyPublisher } -final class ServiceGetBusPosByRtidFetcher: GetBusPosByRtidFetchable { - func fetch(param: [String : String], on queue: DispatchQueue) -> AnyPublisher { +struct ServiceGetBusPosByRtidFetcher: ServiceFetchable, GetBusPosByRtidFetchable { + private(set) var networkService: NetworkServiceProtocol + private(set) var tokenManager: TokenManagable + private(set) var requestFactory: Requestable + + func fetch(param: [String : String]) -> AnyPublisher { let url = "http://ws.bus.go.kr/api/rest/buspos/getBusPosByRtid" - return Service.shared.get(url: url, params: param, on: queue).mapJsonBBusAPIError() + return self.fetch(url: url, param: param) } } diff --git a/BBus/BBus/Global/Network/Fetcher/GetBusPosByVehIdFetcher.swift b/BBus/BBus/Global/Network/Fetcher/GetBusPosByVehIdFetcher.swift index 393fb983..146bfc3c 100644 --- a/BBus/BBus/Global/Network/Fetcher/GetBusPosByVehIdFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/GetBusPosByVehIdFetcher.swift @@ -9,12 +9,16 @@ import Foundation import Combine protocol GetBusPosByVehIdFetchable { - func fetch(param: [String: String], on queue: DispatchQueue) -> AnyPublisher + func fetch(param: [String: String]) -> AnyPublisher } -final class ServiceGetBusPosByVehIdFetcher: GetBusPosByVehIdFetchable { - func fetch(param: [String: String], on queue: DispatchQueue) -> AnyPublisher { +struct ServiceGetBusPosByVehIdFetcher: ServiceFetchable, GetBusPosByVehIdFetchable { + private(set) var networkService: NetworkServiceProtocol + private(set) var tokenManager: TokenManagable + private(set) var requestFactory: Requestable + + func fetch(param: [String: String]) -> AnyPublisher { let url = "http://ws.bus.go.kr/api/rest/buspos/getBusPosByVehId" - return Service.shared.get(url: url, params: param, on: queue).mapJsonBBusAPIError() + return self.fetch(url: url, param: param) } } diff --git a/BBus/BBus/Global/Network/Fetcher/GetFavoriteItemListFetcher.swift b/BBus/BBus/Global/Network/Fetcher/GetFavoriteItemListFetcher.swift index 1cecb9df..481d0108 100644 --- a/BBus/BBus/Global/Network/Fetcher/GetFavoriteItemListFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/GetFavoriteItemListFetcher.swift @@ -9,11 +9,14 @@ import Foundation import Combine protocol GetFavoriteItemListFetchable { - func fetch(on queue: DispatchQueue) -> AnyPublisher + func fetch() -> AnyPublisher } -final class PersistentGetFavoriteItemListFetcher: GetFavoriteItemListFetchable { - func fetch(on queue: DispatchQueue) -> AnyPublisher { - return Persistent.shared.getFromUserDefaults(key: "FavoriteItems", on: queue) +struct PersistenceGetFavoriteItemListFetcher: PersistenceFetchable, GetFavoriteItemListFetchable { + private(set) var persistenceStorage: PersistenceStorageProtocol + + func fetch() -> AnyPublisher { + return self.persistenceStorage.getFromUserDefaults(key: "FavoriteItems") + .eraseToAnyPublisher() } } diff --git a/BBus/BBus/Global/Network/Fetcher/GetRouteInfoItemFetcher.swift b/BBus/BBus/Global/Network/Fetcher/GetRouteInfoItemFetcher.swift index 18e58e85..3ab4fd35 100644 --- a/BBus/BBus/Global/Network/Fetcher/GetRouteInfoItemFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/GetRouteInfoItemFetcher.swift @@ -9,12 +9,16 @@ import Foundation import Combine protocol GetRouteInfoItemFetchable { - func fetch(param: [String: String], on queue: DispatchQueue) -> AnyPublisher + func fetch(param: [String: String]) -> AnyPublisher } -final class ServiceGetRouteInfoItemFetcher: GetRouteInfoItemFetchable { - func fetch(param: [String : String], on queue: DispatchQueue) -> AnyPublisher { +struct ServiceGetRouteInfoItemFetcher: ServiceFetchable, GetRouteInfoItemFetchable { + private(set) var networkService: NetworkServiceProtocol + private(set) var tokenManager: TokenManagable + private(set) var requestFactory: Requestable + + func fetch(param: [String : String]) -> AnyPublisher { let url = "http://ws.bus.go.kr/api/rest/busRouteInfo/getRouteInfo" - return Service.shared.get(url: url, params: param, on: queue).mapJsonBBusAPIError() + return self.fetch(url: url, param: param) } } diff --git a/BBus/BBus/Global/Network/Fetcher/GetRouteListFetcher.swift b/BBus/BBus/Global/Network/Fetcher/GetRouteListFetcher.swift index 86e01013..40263bf4 100644 --- a/BBus/BBus/Global/Network/Fetcher/GetRouteListFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/GetRouteListFetcher.swift @@ -9,11 +9,14 @@ import Foundation import Combine protocol GetRouteListFetchable { - func fetch(on queue: DispatchQueue) -> AnyPublisher + func fetch() -> AnyPublisher } -final class PersistentGetRouteListFetcher: GetRouteListFetchable { - func fetch(on queue: DispatchQueue) -> AnyPublisher { - return Persistent.shared.get(file: "BusRouteList", type: "json", on: queue) +struct PersistenceGetRouteListFetcher: PersistenceFetchable, GetRouteListFetchable { + private(set) var persistenceStorage: PersistenceStorageProtocol + + func fetch() -> AnyPublisher { + return self.persistenceStorage.get(file: "BusRouteList", type: "json") + .eraseToAnyPublisher() } } diff --git a/BBus/BBus/Global/Network/Fetcher/GetStationByUidItemFetcher.swift b/BBus/BBus/Global/Network/Fetcher/GetStationByUidItemFetcher.swift index 0b9eff22..bfe48f14 100644 --- a/BBus/BBus/Global/Network/Fetcher/GetStationByUidItemFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/GetStationByUidItemFetcher.swift @@ -9,12 +9,16 @@ import Foundation import Combine protocol GetStationByUidItemFetchable { - func fetch(param: [String: String], on queue: DispatchQueue) -> AnyPublisher + func fetch(param: [String: String]) -> AnyPublisher } -final class ServiceGetStationByUidItemFetcher: GetStationByUidItemFetchable { - func fetch(param: [String : String], on queue: DispatchQueue) -> AnyPublisher { +struct ServiceGetStationByUidItemFetcher: ServiceFetchable, GetStationByUidItemFetchable { + private(set) var networkService: NetworkServiceProtocol + private(set) var tokenManager: TokenManagable + private(set) var requestFactory: Requestable + + func fetch(param: [String : String]) -> AnyPublisher { let url = "http://ws.bus.go.kr/api/rest/stationinfo/getStationByUid" - return Service.shared.get(url: url, params: param, on: queue).mapJsonBBusAPIError() + return self.fetch(url: url, param: param) } } diff --git a/BBus/BBus/Global/Network/Fetcher/GetStationListFetcher.swift b/BBus/BBus/Global/Network/Fetcher/GetStationListFetcher.swift index bc7b0ebd..9aec830a 100644 --- a/BBus/BBus/Global/Network/Fetcher/GetStationListFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/GetStationListFetcher.swift @@ -9,11 +9,14 @@ import Foundation import Combine protocol GetStationListFetchable { - func fetch(on queue: DispatchQueue) -> AnyPublisher + func fetch() -> AnyPublisher } -final class PersistentGetStationListFetcher: GetStationListFetchable { - func fetch(on queue: DispatchQueue) -> AnyPublisher { - return Persistent.shared.get(file: "StationList", type: "json", on: queue) +struct PersistenceGetStationListFetcher: PersistenceFetchable, GetStationListFetchable { + private(set) var persistenceStorage: PersistenceStorageProtocol + + func fetch() -> AnyPublisher { + return self.persistenceStorage.get(file: "StationList", type: "json") + .eraseToAnyPublisher() } } diff --git a/BBus/BBus/Global/Network/Fetcher/GetStationsByRouteListFetcher.swift b/BBus/BBus/Global/Network/Fetcher/GetStationsByRouteListFetcher.swift index 17a29f55..a6f5355f 100644 --- a/BBus/BBus/Global/Network/Fetcher/GetStationsByRouteListFetcher.swift +++ b/BBus/BBus/Global/Network/Fetcher/GetStationsByRouteListFetcher.swift @@ -9,12 +9,16 @@ import Foundation import Combine protocol GetStationsByRouteListFetchable { - func fetch(param: [String: String], on queue: DispatchQueue) -> AnyPublisher + func fetch(param: [String: String]) -> AnyPublisher } -final class ServiceGetStationsByRouteListFetcher: GetStationsByRouteListFetchable { - func fetch(param: [String : String], on queue: DispatchQueue) -> AnyPublisher { +struct ServiceGetStationsByRouteListFetcher: ServiceFetchable, GetStationsByRouteListFetchable { + private(set) var networkService: NetworkServiceProtocol + private(set) var tokenManager: TokenManagable + private(set) var requestFactory: Requestable + + func fetch(param: [String : String]) -> AnyPublisher { let url = "http://ws.bus.go.kr/api/rest/busRouteInfo/getStaionByRoute" - return Service.shared.get(url: url, params: param, on: queue).mapJsonBBusAPIError() + return self.fetch(url: url, param: param) } } diff --git a/BBus/BBus/Global/Network/Fetcher/PersistencetFetchable.swift b/BBus/BBus/Global/Network/Fetcher/PersistencetFetchable.swift new file mode 100644 index 00000000..1af78ae4 --- /dev/null +++ b/BBus/BBus/Global/Network/Fetcher/PersistencetFetchable.swift @@ -0,0 +1,12 @@ +// +// PersistencetFetchable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation + +protocol PersistenceFetchable { + var persistenceStorage: PersistenceStorageProtocol { get } +} diff --git a/BBus/BBus/Global/Network/Fetcher/ServiceFetchable.swift b/BBus/BBus/Global/Network/Fetcher/ServiceFetchable.swift new file mode 100644 index 00000000..f8ae2acd --- /dev/null +++ b/BBus/BBus/Global/Network/Fetcher/ServiceFetchable.swift @@ -0,0 +1,38 @@ +// +// ServiceFetcher.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol ServiceFetchable { + var networkService: NetworkServiceProtocol { get } + var tokenManager: TokenManagable { get } + var requestFactory: Requestable { get } + + func fetch(url: String, param: [String: String]) -> AnyPublisher +} + +extension ServiceFetchable { + func fetch(url: String, param: [String: String]) -> AnyPublisher { + guard let key = try? self.tokenManager.randomAccessKey() else { return BBusAPIError.noMoreAccessKeyError.publisher } + guard let request = + self.requestFactory.request(url: url, accessKey: key.key, params: param) else { return NetworkError.urlError.publisher } + return networkService.get(request: request) + .mapJsonBBusAPIError { + self.tokenManager.removeAccessKey(at: key.index) + } + } +} + +fileprivate extension Error { + var publisher: AnyPublisher { + let publisher = CurrentValueSubject(nil) + publisher.send(completion: .failure(self)) + return publisher.compactMap({$0}) + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/NetworkService.swift b/BBus/BBus/Global/Network/NetworkService.swift new file mode 100644 index 00000000..5b9f936f --- /dev/null +++ b/BBus/BBus/Global/Network/NetworkService.swift @@ -0,0 +1,34 @@ +// +// NetworkService.swift +// BBus +// +// Created by Kang Minsang on 2021/11/10. +// + +import Foundation +import Combine + +enum NetworkError: Error { + case accessKeyError, urlError, unknownError, noDataError, noResponseError, responseError +} + +protocol NetworkServiceProtocol { + func get(request: URLRequest) -> AnyPublisher +} + +struct NetworkService: NetworkServiceProtocol { + func get(request: URLRequest) -> AnyPublisher { + return URLSession.shared.dataTaskPublisher(for: request) + .mapError({ $0 as Error }) + .tryMap { data, response -> Data in + guard let response = response as? HTTPURLResponse else { + throw NetworkError.noResponseError + } + if response.statusCode != 200 { + throw NetworkError.responseError + } + return data + } + .eraseToAnyPublisher() + } +} diff --git a/BBus/BBus/Global/Network/PersistenceStorage.swift b/BBus/BBus/Global/Network/PersistenceStorage.swift new file mode 100644 index 00000000..b7ede279 --- /dev/null +++ b/BBus/BBus/Global/Network/PersistenceStorage.swift @@ -0,0 +1,113 @@ +// +// PersistenceStorage.swift +// BBus +// +// Created by Kang Minsang on 2021/11/10. +// + +import Foundation +import Combine + +protocol PersistenceStorageProtocol { + func create(key: String, param: T) -> AnyPublisher + func getFromUserDefaults(key: String) -> AnyPublisher + func get(file: String, type: String) -> AnyPublisher + func delete(key: String, param: T) -> AnyPublisher +} + +struct PersistenceStorage: PersistenceStorageProtocol { + + private enum PersistenceError: Error { + case noneError, decodingError, encodingError, urlError + } + + func create(key: String, param: T) -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + DispatchQueue.global().async { [weak publisher] in + var items: [T] = [] + if let data = UserDefaults.standard.data(forKey: key) { + let decodingItems = (try? PropertyListDecoder().decode([T].self, from: data)) + if let decodingItems = decodingItems { + items = decodingItems + } else { + publisher?.send(completion: .failure(PersistenceError.decodingError)) + } + } + items.append(param) + if let data = try? PropertyListEncoder().encode(items) { + UserDefaults.standard.set(data, forKey: key) + } else { + publisher?.send(completion: .failure(PersistenceError.encodingError)) + return + } + if let item = try? PropertyListEncoder().encode(param) { + publisher?.send(item) + } else { + publisher?.send(completion: .failure(PersistenceError.encodingError)) + } + } + return publisher.compactMap({$0}).eraseToAnyPublisher() + } + + func getFromUserDefaults(key: String) -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + DispatchQueue.global().async { [weak publisher] in + if let data = UserDefaults.standard.data(forKey: key) { + publisher?.send(data) + } else { + let emptyFavoriteList = [FavoriteItemDTO]() + + if let data = try? PropertyListEncoder().encode(emptyFavoriteList) { + publisher?.send(data) + } else { + publisher?.send(completion: .failure(PersistenceError.noneError)) + } + } + } + return publisher.compactMap({$0}).eraseToAnyPublisher() + } + + func get(file: String, type: String) -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + DispatchQueue.global().async { [weak publisher] in + guard let url = Bundle.main.url(forResource: file, withExtension: type) else { + publisher?.send(completion: .failure(PersistenceError.urlError)) + return + } + if let data = try? Data(contentsOf: url) { + publisher?.send(data) + } else { + publisher?.send(completion: .failure(PersistenceError.noneError)) + } + } + return publisher.compactMap({$0}).eraseToAnyPublisher() + } + + func delete(key: String, param: T) -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + DispatchQueue.global().async { [weak publisher] in + guard let data = UserDefaults.standard.data(forKey: key) else { + publisher?.send(completion: .failure(PersistenceError.noneError)) + return + } + guard var items = (try? PropertyListDecoder().decode([T].self, from: data)) else { + publisher?.send(completion: .failure(PersistenceError.decodingError)) + return + } + let count = items.count + items.removeAll() {$0 == param} + if count == items.count { + publisher?.send(completion: .failure(PersistenceError.noneError)) + return + } + if let data = try? PropertyListEncoder().encode(items) { + UserDefaults.standard.set(data, forKey: key) + publisher?.send(data) + } else { + publisher?.send(completion: .failure(PersistenceError.encodingError)) + } + } + return publisher.compactMap({$0}).eraseToAnyPublisher() + } +} + diff --git a/BBus/BBus/Global/Network/Persistent.swift b/BBus/BBus/Global/Network/PersistentStorage.swift similarity index 66% rename from BBus/BBus/Global/Network/Persistent.swift rename to BBus/BBus/Global/Network/PersistentStorage.swift index c36f185b..70c122e2 100644 --- a/BBus/BBus/Global/Network/Persistent.swift +++ b/BBus/BBus/Global/Network/PersistentStorage.swift @@ -1,5 +1,5 @@ // -// Persistent.swift +// PersistentStorage.swift // BBus // // Created by Kang Minsang on 2021/11/10. @@ -8,19 +8,24 @@ import Foundation import Combine -final class Persistent { +protocol PersistentStorageProtocol { + func create(key: String, param: T) -> AnyPublisher + func getFromUserDefaults(key: String) -> AnyPublisher + func get(file: String, type: String) -> AnyPublisher + func delete(key: String, param: T) -> AnyPublisher +} + +final class PersistenceStorage: PersistentStorageProtocol { enum PersistentError: Error { case noneError, decodingError, encodingError, urlError } - static let shared = Persistent() - - private init() { } + static let shared = PersistenceStorage() - func create(key: String, param: T, on queue: DispatchQueue) -> AnyPublisher { - let publisher = PassthroughSubject() - queue.async { [weak publisher] in + func create(key: String, param: T) -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + DispatchQueue.global().async { [weak publisher] in var items: [T] = [] if let data = UserDefaults.standard.data(forKey: key) { let decodingItems = (try? PropertyListDecoder().decode([T].self, from: data)) @@ -43,12 +48,12 @@ final class Persistent { publisher?.send(completion: .failure(PersistentError.encodingError)) } } - return publisher.eraseToAnyPublisher() + return publisher.compactMap({$0}).eraseToAnyPublisher() } - func getFromUserDefaults(key: String, on queue: DispatchQueue) -> AnyPublisher { - let publisher = PassthroughSubject() - queue.async { [weak publisher] in + func getFromUserDefaults(key: String) -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + DispatchQueue.global().async { [weak publisher] in if let data = UserDefaults.standard.data(forKey: key) { publisher?.send(data) } else { @@ -61,12 +66,12 @@ final class Persistent { } } } - return publisher.eraseToAnyPublisher() + return publisher.compactMap({$0}).eraseToAnyPublisher() } - func get(file: String, type: String, on queue: DispatchQueue) -> AnyPublisher { - let publisher = PassthroughSubject() - queue.async { [weak publisher] in + func get(file: String, type: String) -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + DispatchQueue.global().async { [weak publisher] in guard let url = Bundle.main.url(forResource: file, withExtension: type) else { publisher?.send(completion: .failure(PersistentError.urlError)) return @@ -77,12 +82,12 @@ final class Persistent { publisher?.send(completion: .failure(PersistentError.noneError)) } } - return publisher.eraseToAnyPublisher() + return publisher.compactMap({$0}).eraseToAnyPublisher() } - func delete(key: String, param: T, on queue: DispatchQueue) -> AnyPublisher { - let publisher = PassthroughSubject() - queue.async { [weak publisher] in + func delete(key: String, param: T) -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + DispatchQueue.global().async { [weak publisher] in guard let data = UserDefaults.standard.data(forKey: key) else { publisher?.send(completion: .failure(PersistentError.noneError)) return @@ -104,7 +109,7 @@ final class Persistent { publisher?.send(completion: .failure(PersistentError.encodingError)) } } - return publisher.eraseToAnyPublisher() + return publisher.compactMap({$0}).eraseToAnyPublisher() } } diff --git a/BBus/BBus/Global/Network/RequestFactory.swift b/BBus/BBus/Global/Network/RequestFactory.swift new file mode 100644 index 00000000..2a8613ba --- /dev/null +++ b/BBus/BBus/Global/Network/RequestFactory.swift @@ -0,0 +1,29 @@ +// +// RequestFactory.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation + +protocol Requestable { + func request(url: String, accessKey: String, params: [String: String]) -> URLRequest? +} + +struct RequestFactory: Requestable { + func request(url: String, accessKey: String, params: [String: String]) -> URLRequest? { + guard var components = URLComponents(string: url) else { return nil } + var items: [URLQueryItem] = [] + params.forEach() { item in + items.append(URLQueryItem(name: item.key, value: item.value)) + } + components.queryItems = items + guard let query = components.percentEncodedQuery else { return nil } + components.percentEncodedQuery = query + "&serviceKey=" + accessKey + guard let url = components.url else { return nil } + var request = URLRequest(url: url) + request.httpMethod = "GET" + return request + } +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/CreateFavoriteItemUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/CreateFavoriteItemUsable.swift new file mode 100644 index 00000000..a518e6b2 --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/CreateFavoriteItemUsable.swift @@ -0,0 +1,13 @@ +// +// CreateFavoriteItemUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol CreateFavoriteItemUsable { + func createFavoriteItem(param: FavoriteItemDTO) -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/DeleteFavoriteItemUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/DeleteFavoriteItemUsable.swift new file mode 100644 index 00000000..3d4fdb24 --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/DeleteFavoriteItemUsable.swift @@ -0,0 +1,13 @@ +// +// DeleteFavoriteItemUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol DeleteFavoriteItemUsable { + func deleteFavoriteItem(param: FavoriteItemDTO) -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/GetArrInfoByRouteListUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/GetArrInfoByRouteListUsable.swift new file mode 100644 index 00000000..d5a866ee --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/GetArrInfoByRouteListUsable.swift @@ -0,0 +1,13 @@ +// +// GetArrInfoByRouteListUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol GetArrInfoByRouteListUsable { + func getArrInfoByRouteList(stId: String, busRouteId: String, ord: String) -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/GetBusPosByRtidUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/GetBusPosByRtidUsable.swift new file mode 100644 index 00000000..516197de --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/GetBusPosByRtidUsable.swift @@ -0,0 +1,13 @@ +// +// GetBusPosByRtidUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol GetBusPosByRtidUsable { + func getBusPosByRtid(busRoutedId: String) -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/GetBusPosByVehIdUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/GetBusPosByVehIdUsable.swift new file mode 100644 index 00000000..3f3bdb26 --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/GetBusPosByVehIdUsable.swift @@ -0,0 +1,13 @@ +// +// GetBusPosByVehIdUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol GetBusPosByVehIdUsable { + func getBusPosByVehId(_ vehId: String) -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/GetFavoriteItemListUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/GetFavoriteItemListUsable.swift new file mode 100644 index 00000000..bb483397 --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/GetFavoriteItemListUsable.swift @@ -0,0 +1,13 @@ +// +// GetFavoriteItemListUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol GetFavoriteItemListUsable { + func getFavoriteItemList() -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/GetRouteListUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/GetRouteListUsable.swift new file mode 100644 index 00000000..e5c0e26e --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/GetRouteListUsable.swift @@ -0,0 +1,13 @@ +// +// GetRouteListUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol GetRouteListUsable { + func getRouteList() -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/GetStationByUidItemUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/GetStationByUidItemUsable.swift new file mode 100644 index 00000000..eaeff42e --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/GetStationByUidItemUsable.swift @@ -0,0 +1,13 @@ +// +// GetStationByUidItemUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol GetStationByUidItemUsable { + func getStationByUidItem(arsId: String) -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/GetStationListUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/GetStationListUsable.swift new file mode 100644 index 00000000..249fba69 --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/GetStationListUsable.swift @@ -0,0 +1,13 @@ +// +// GetStationListUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol GetStationListUsable { + func getStationList() -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUseCases/GetStationsByRouteListUsable.swift b/BBus/BBus/Global/Network/RequestUseCases/GetStationsByRouteListUsable.swift new file mode 100644 index 00000000..5769097f --- /dev/null +++ b/BBus/BBus/Global/Network/RequestUseCases/GetStationsByRouteListUsable.swift @@ -0,0 +1,13 @@ +// +// GetStationsByRouteListUsable.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation +import Combine + +protocol GetStationsByRouteListUsable { + func getStationsByRouteList(busRoutedId: String) -> AnyPublisher +} diff --git a/BBus/BBus/Global/Network/RequestUsecases.swift b/BBus/BBus/Global/Network/RequestUsecases.swift deleted file mode 100644 index 955025ed..00000000 --- a/BBus/BBus/Global/Network/RequestUsecases.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// RequestUsecases.swift -// BBus -// -// Created by Kang Minsang on 2021/11/10. -// - -import Foundation -import Combine - -typealias RequestUsecases = (GetArrInfoByRouteListUsecase & GetStationsByRouteListUsecase & GetBusPosByRtidUsecase & GetStationByUidItemUsecase & GetRouteListUsecase & GetStationListUsecase & GetFavoriteItemListUsecase & CreateFavoriteItemUsecase & DeleteFavoriteItemUsecase & GetBusPosByVehIdUsecase) - -// MARK: - API Protocol -protocol GetArrInfoByRouteListUsecase { - func getArrInfoByRouteList(stId: String, busRouteId: String, ord: String) -> AnyPublisher -} - -protocol GetStationsByRouteListUsecase { - func getStationsByRouteList(busRoutedId: String) -> AnyPublisher -} - -protocol GetBusPosByRtidUsecase { - func getBusPosByRtid(busRoutedId: String) -> AnyPublisher -} - -protocol GetStationByUidItemUsecase { - func getStationByUidItem(arsId: String) -> AnyPublisher -} - -protocol GetRouteListUsecase { - func getRouteList() -> AnyPublisher -} - -protocol GetStationListUsecase { - func getStationList() -> AnyPublisher -} - -protocol GetFavoriteItemListUsecase { - func getFavoriteItemList() -> AnyPublisher -} - -protocol CreateFavoriteItemUsecase { - func createFavoriteItem(param: FavoriteItemDTO) -> AnyPublisher -} - -protocol DeleteFavoriteItemUsecase { - func deleteFavoriteItem(param: FavoriteItemDTO) -> AnyPublisher -} - -protocol GetBusPosByVehIdUsecase { - func getBusPosByVehId(_ vehId: String) -> AnyPublisher -} diff --git a/BBus/BBus/Global/Network/Service.swift b/BBus/BBus/Global/Network/Service.swift deleted file mode 100644 index 6c90196b..00000000 --- a/BBus/BBus/Global/Network/Service.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// Service.swift -// BBus -// -// Created by Kang Minsang on 2021/11/10. -// - -import Foundation -import Combine - -enum NetworkError: Error { - case accessKeyError, urlError, unknownError, noDataError, noResponseError, responseError -} - -// TODO: - Service Return Type 수정 필요 -final class Service { - static let shared = Service() - - private var accessKeys: [String] = { () -> [String] in - let keys = [Bundle.main.infoDictionary?["API_ACCESS_KEY1"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY2"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY3"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY4"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY5"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY6"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY7"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY8"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY9"] as? String, - Bundle.main.infoDictionary?["API_ACCESS_KEY10"] as? String, - Bundle.main.infoDictionary?["API_ACCESS_KEY11"] as? String, - Bundle.main.infoDictionary?["API_ACCESS_KEY12"] as? String, - Bundle.main.infoDictionary?["API_ACCESS_KEY13"] as? String, - Bundle.main.infoDictionary?["API_ACCESS_KEY14"] as? String, - Bundle.main.infoDictionary?["API_ACCESS_KEY15"] as? String, - Bundle.main.infoDictionary?["API_ACCESS_KEY16"] as? String, - Bundle.main.infoDictionary?["API_ACCESS_KEY17"] as? String] - return keys.compactMap({ $0 }) - }() - - private var keys: [Int] = [] - - private init() { - self.keys = Array(0.. AnyPublisher<(Data, Int), Error> { - let userDefaultKey = "APIRequestCount" - let apiRequestCount = UserDefaults.standard.object(forKey: userDefaultKey) as? Int ?? 0 - if apiRequestCount > 500 { - let publisher = PassthroughSubject<(Data, Int), Error>() - queue.async { - publisher.send(completion: .failure(BBusAPIError.trafficExceed)) - } - return publisher.eraseToAnyPublisher() - } - UserDefaults.standard.set(apiRequestCount + 1, forKey: userDefaultKey) - guard self.keys.count != 0, - let order = self.keys.randomElement() else { - let publisher = PassthroughSubject<(Data, Int), Error>() - queue.async { - publisher.send(completion: .failure(BBusAPIError.noMoreAccessKeyError)) - } - return publisher.eraseToAnyPublisher() - } - if let request = self.makeRequest(url: url, accessKey: self.accessKeys[order], params: params) { - return URLSession.shared.dataTaskPublisher(for: request) - .mapError({ $0 as Error }) - .tryMap { data, response -> (Data, Int) in - guard let response = response as? HTTPURLResponse else { - throw NetworkError.noResponseError - } - if response.statusCode != 200 { - throw NetworkError.responseError - } - return (data, order) - } - .eraseToAnyPublisher() - } - else { - let publisher = PassthroughSubject<(Data, Int), Error>() - queue.async { - publisher.send(completion: .failure(NetworkError.urlError)) - } - return publisher.eraseToAnyPublisher() - } - } - - private func makeRequest(url: String, accessKey: String, params: [String: String]) -> URLRequest? { - guard var components = URLComponents(string: url) else { return nil } - var items: [URLQueryItem] = [] - params.forEach() { item in - items.append(URLQueryItem(name: item.key, value: item.value)) - } - components.queryItems = items - guard let query = components.percentEncodedQuery else { return nil } - components.percentEncodedQuery = query + "&serviceKey=" + accessKey - guard let url = components.url else { return nil } - var request = URLRequest(url: url) - request.httpMethod = "GET" - return request - } -} diff --git a/BBus/BBus/Global/Network/TokenManager.swift b/BBus/BBus/Global/Network/TokenManager.swift new file mode 100644 index 00000000..72356dbc --- /dev/null +++ b/BBus/BBus/Global/Network/TokenManager.swift @@ -0,0 +1,49 @@ +// +// TokenManager.swift +// BBus +// +// Created by 이지수 on 2021/11/29. +// + +import Foundation + +protocol TokenManagable: AnyObject { + init() + func removeAccessKey(at order: Int) + func randomAccessKey() throws -> (index: Int, key: String) +} + +class TokenManager: TokenManagable { + static let maxTokenCount: Int = 17 + + private(set) var accessKeys: [String] = { () -> [String] in + let keys = [Bundle.main.infoDictionary?["API_ACCESS_KEY1"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY2"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY3"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY4"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY5"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY6"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY7"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY8"] as? String, Bundle.main.infoDictionary?["API_ACCESS_KEY9"] as? String, + Bundle.main.infoDictionary?["API_ACCESS_KEY10"] as? String, + Bundle.main.infoDictionary?["API_ACCESS_KEY11"] as? String, + Bundle.main.infoDictionary?["API_ACCESS_KEY12"] as? String, + Bundle.main.infoDictionary?["API_ACCESS_KEY13"] as? String, + Bundle.main.infoDictionary?["API_ACCESS_KEY14"] as? String, + Bundle.main.infoDictionary?["API_ACCESS_KEY15"] as? String, + Bundle.main.infoDictionary?["API_ACCESS_KEY16"] as? String, + Bundle.main.infoDictionary?["API_ACCESS_KEY17"] as? String] + return keys.compactMap({ $0 }).filter({ $0 != "" }) + }() + + private var keys: [Int] + + required init() { + self.keys = Array(0.. (index: Int, key: String) { + guard self.keys.count != 0, + let order = self.keys.randomElement() else { + throw BBusAPIError.noMoreAccessKeyError + } + return (order, accessKeys[order]) + } +} diff --git a/BBus/BBus/Global/Resource/BusRouteList.json b/BBus/BBus/Global/Resource/BusRouteList.json index 38d4deec..9cdc2201 100644 --- a/BBus/BBus/Global/Resource/BusRouteList.json +++ b/BBus/BBus/Global/Resource/BusRouteList.json @@ -1,2480 +1,4405 @@ [ - { - "routeID": 100100006, - "busRouteName": "101", - "routeType": "간선", - "startStation": "우이동", - "endStation": "서소문" - }, - { - "routeID": 100100007, - "busRouteName": "102", - "routeType": "간선", - "startStation": "상계주공7단지", - "endStation": "동대문" - }, - { - "routeID": 100100008, - "busRouteName": "103", - "routeType": "간선", - "startStation": "삼화상운", - "endStation": "서울역" - }, - { - "routeID": 100100009, - "busRouteName": "104", - "routeType": "간선", - "startStation": "강북청소년수련관", - "endStation": "서울역버스환승센터" - }, - { - "routeID": 100100010, - "busRouteName": "105", - "routeType": "간선", - "startStation": "상계주공7단지", - "endStation": "서울역" - }, - { - "routeID": 100100011, - "busRouteName": "106", - "routeType": "간선", - "startStation": "의정부", - "endStation": "종로5가" - }, - { - "routeID": 100100012, - "busRouteName": "107", - "routeType": "간선", - "startStation": "민락동차고지", - "endStation": "동대문" - }, - { - "routeID": 100100014, - "busRouteName": "109", - "routeType": "간선", - "startStation": "우이동", - "endStation": "광화문" - }, - { - "routeID": 100100017, - "busRouteName": "120", - "routeType": "간선", - "startStation": "우이동", - "endStation": "청량리" - }, - { - "routeID": 100100018, - "busRouteName": "130", - "routeType": "간선", - "startStation": "우이동", - "endStation": "길동" - }, - { - "routeID": 100100019, - "busRouteName": "140", - "routeType": "간선", - "startStation": "도봉산역광역환승센터", - "endStation": "AT센터양재꽃시장" - }, - { - "routeID": 100100020, - "busRouteName": "141", - "routeType": "간선", - "startStation": "도봉산", - "endStation": "염곡동" - }, - { - "routeID": 100100021, - "busRouteName": "142", - "routeType": "간선", - "startStation": "방배동", - "endStation": "도봉산" - }, - { - "routeID": 100100022, - "busRouteName": "143", - "routeType": "간선", - "startStation": "정릉", - "endStation": "개포동" - }, - { - "routeID": 100100023, - "busRouteName": "144", - "routeType": "간선", - "startStation": "우이동", - "endStation": "교대" - }, - { - "routeID": 100100024, - "busRouteName": "145", - "routeType": "간선", - "startStation": "번동", - "endStation": "강남역" - }, - { - "routeID": 100100025, - "busRouteName": "146", - "routeType": "간선", - "startStation": "상계주공7단지", - "endStation": "강남역" - }, - { - "routeID": 100100026, - "busRouteName": "147", - "routeType": "간선", - "startStation": "월계동", - "endStation": "도곡동" - }, - { - "routeID": 100100027, - "busRouteName": "148", - "routeType": "간선", - "startStation": "번동", - "endStation": "방배동" - }, - { - "routeID": 100100029, - "busRouteName": "150", - "routeType": "간선", - "startStation": "도봉산역", - "endStation": "시흥대교" - }, - { - "routeID": 100100030, - "busRouteName": "151", - "routeType": "간선", - "startStation": "우이동", - "endStation": "중앙대" - }, - { - "routeID": 100100031, - "busRouteName": "152", - "routeType": "간선", - "startStation": "화계사", - "endStation": "삼막사사거리" - }, - { - "routeID": 100100032, - "busRouteName": "153", - "routeType": "간선", - "startStation": "우이동", - "endStation": "당곡사거리" - }, - { - "routeID": 100100033, - "busRouteName": "160", - "routeType": "간선", - "startStation": "도봉산역", - "endStation": "온수동종점" - }, - { - "routeID": 100100034, - "busRouteName": "162", - "routeType": "간선", - "startStation": "정릉", - "endStation": "여의도" - }, - { - "routeID": 100100036, - "busRouteName": "171", - "routeType": "간선", - "startStation": "국민대앞", - "endStation": "월드컵파크7단지" - }, - { - "routeID": 100100037, - "busRouteName": "172", - "routeType": "간선", - "startStation": "하계동", - "endStation": "상암동" - }, - { - "routeID": 100100038, - "busRouteName": "201", - "routeType": "간선", - "startStation": "수택동차고지", - "endStation": "서울역환승센터" - }, - { - "routeID": 100100039, - "busRouteName": "202", - "routeType": "간선", - "startStation": "불암동", - "endStation": "후암동" - }, - { - "routeID": 100100042, - "busRouteName": "260", - "routeType": "간선", - "startStation": "중랑공영차고지", - "endStation": "국회의사당" - }, - { - "routeID": 100100043, - "busRouteName": "261", - "routeType": "간선", - "startStation": "석관동(상진운수종점)", - "endStation": "여의도" - }, - { - "routeID": 100100044, - "busRouteName": "262", - "routeType": "간선", - "startStation": "중랑공영차고지", - "endStation": "여의도환승센타" - }, - { - "routeID": 100100046, - "busRouteName": "270", - "routeType": "간선", - "startStation": "상암차고지", - "endStation": "망우리(출)" - }, - { - "routeID": 100100047, - "busRouteName": "271", - "routeType": "간선", - "startStation": "용마문화복지센터", - "endStation": "월드컵파크7단지" - }, - { - "routeID": 100100048, - "busRouteName": "272", - "routeType": "간선", - "startStation": "면목동", - "endStation": "남가좌동" - }, - { - "routeID": 100100049, - "busRouteName": "273", - "routeType": "간선", - "startStation": "중랑공영차고지", - "endStation": "홍대입구역" - }, - { - "routeID": 100100051, - "busRouteName": "301", - "routeType": "간선", - "startStation": "장지공영차고지", - "endStation": "혜화동" - }, - { - "routeID": 100100052, - "busRouteName": "302", - "routeType": "간선", - "startStation": "상대원차고지", - "endStation": "상왕십리역" - }, - { - "routeID": 100100053, - "busRouteName": "303", - "routeType": "간선", - "startStation": "상대원차고지", - "endStation": "신설동역" - }, - { - "routeID": 100100055, - "busRouteName": "340", - "routeType": "간선", - "startStation": "강동공영차고지", - "endStation": "강남역" - }, - { - "routeID": 100100056, - "busRouteName": "341", - "routeType": "간선", - "startStation": "하남공영차고지", - "endStation": "강남역" - }, - { - "routeID": 100100057, - "busRouteName": "360", - "routeType": "간선", - "startStation": "송파차고지", - "endStation": "여의도환승센터" - }, - { - "routeID": 100100061, - "busRouteName": "370", - "routeType": "간선", - "startStation": "강동공영차고지", - "endStation": "충정로역" - }, - { - "routeID": 100100062, - "busRouteName": "401", - "routeType": "간선", - "startStation": "장지공영차고지", - "endStation": "서울역" - }, - { - "routeID": 100100063, - "busRouteName": "402", - "routeType": "간선", - "startStation": "장지공영차고지", - "endStation": "광화문" - }, - { - "routeID": 100100064, - "busRouteName": "406", - "routeType": "간선", - "startStation": "개포동", - "endStation": "서울역" - }, - { - "routeID": 100100068, - "busRouteName": "420", - "routeType": "간선", - "startStation": "개포동", - "endStation": "청량리" - }, - { - "routeID": 100100070, - "busRouteName": "441", - "routeType": "간선", - "startStation": "월암공영차고지", - "endStation": "신사역" - }, - { - "routeID": 100100071, - "busRouteName": "461", - "routeType": "간선", - "startStation": "장지공영차고지", - "endStation": "여의도" - }, - { - "routeID": 100100073, - "busRouteName": "470", - "routeType": "간선", - "startStation": "상암차고지", - "endStation": "안골마을" - }, - { - "routeID": 100100075, - "busRouteName": "472", - "routeType": "간선", - "startStation": "개포동차고지", - "endStation": "신촌로타리" - }, - { - "routeID": 100100076, - "busRouteName": "500", - "routeType": "간선", - "startStation": "석수역", - "endStation": "을지로입구" - }, - { - "routeID": 100100077, - "busRouteName": "501", - "routeType": "간선", - "startStation": "서울대학교", - "endStation": "종로2가" - }, - { - "routeID": 100100078, - "busRouteName": "503", - "routeType": "간선", - "startStation": "광명공영차고지", - "endStation": "서울역" - }, - { - "routeID": 100100079, - "busRouteName": "504", - "routeType": "간선", - "startStation": "광명공영주차장", - "endStation": "남대문" - }, - { - "routeID": 100100080, - "busRouteName": "505", - "routeType": "간선", - "startStation": "노온사동", - "endStation": "서울역" - }, - { - "routeID": 100100081, - "busRouteName": "506", - "routeType": "간선", - "startStation": "신림2동차고지", - "endStation": "을지로입구역광교" - }, - { - "routeID": 100100082, - "busRouteName": "507", - "routeType": "간선", - "startStation": "석수역", - "endStation": "동대문역사문화공원" - }, - { - "routeID": 100100083, - "busRouteName": "540", - "routeType": "간선", - "startStation": "군포공영차고지", - "endStation": "서울성모병원" - }, - { - "routeID": 100100084, - "busRouteName": "571", - "routeType": "간선", - "startStation": "가산동", - "endStation": "은평뉴타운공영차고지" - }, - { - "routeID": 100100085, - "busRouteName": "600", - "routeType": "간선", - "startStation": "온수동", - "endStation": "광화문" - }, - { - "routeID": 100100086, - "busRouteName": "601", - "routeType": "간선", - "startStation": "개화동", - "endStation": "혜화역" - }, - { - "routeID": 100100087, - "busRouteName": "602", - "routeType": "간선", - "startStation": "양천공영차고지", - "endStation": "시청앞" - }, - { - "routeID": 100100088, - "busRouteName": "603", - "routeType": "간선", - "startStation": "신월동", - "endStation": "시청" - }, - { - "routeID": 100100089, - "busRouteName": "604", - "routeType": "간선", - "startStation": "신월동기점", - "endStation": "중구청앞" - }, - { - "routeID": 100100090, - "busRouteName": "605", - "routeType": "간선", - "startStation": "강서공영차고지.개화역", - "endStation": "후암동" - }, - { - "routeID": 100100091, - "busRouteName": "606", - "routeType": "간선", - "startStation": "부천상동", - "endStation": "조계사" - }, - { - "routeID": 100100093, - "busRouteName": "640", - "routeType": "간선", - "startStation": "신월동기점", - "endStation": "강남역" - }, - { - "routeID": 100100094, - "busRouteName": "641", - "routeType": "간선", - "startStation": "문래동", - "endStation": "양재동" - }, - { - "routeID": 100100096, - "busRouteName": "643", - "routeType": "간선", - "startStation": "양천차고지", - "endStation": "강남역" - }, - { - "routeID": 100100097, - "busRouteName": "650", - "routeType": "간선", - "startStation": "외발산동", - "endStation": "낙성대입구" - }, - { - "routeID": 100100098, - "busRouteName": "651", - "routeType": "간선", - "startStation": "방화동", - "endStation": "관악구청" - }, - { - "routeID": 100100099, - "busRouteName": "652", - "routeType": "간선", - "startStation": "신월동기점", - "endStation": "금천우체국독산1동주민센터" - }, - { - "routeID": 100100102, - "busRouteName": "661", - "routeType": "간선", - "startStation": "부천상동", - "endStation": "신세계백화점" - }, - { - "routeID": 100100103, - "busRouteName": "701", - "routeType": "간선", - "startStation": "진관차고지", - "endStation": "종로2가삼일교" - }, - { - "routeID": 100100107, - "busRouteName": "704", - "routeType": "간선", - "startStation": "송추", - "endStation": "서울역버스환승센터" - }, - { - "routeID": 100100110, - "busRouteName": "710", - "routeType": "간선", - "startStation": "상암차고지", - "endStation": "수유역강북구청" - }, - { - "routeID": 100100111, - "busRouteName": "720", - "routeType": "간선", - "startStation": "진관공영차고지", - "endStation": "답십리" - }, - { - "routeID": 100100112, - "busRouteName": "721", - "routeType": "간선", - "startStation": "북가좌동", - "endStation": "건대입구역" - }, - { - "routeID": 100100114, - "busRouteName": "750A", - "routeType": "간선", - "startStation": "덕은동종점", - "endStation": "서울대학교" - }, - { - "routeID": 100100116, - "busRouteName": "742", - "routeType": "간선", - "startStation": "구산동", - "endStation": "상도동" - }, - { - "routeID": 100100117, - "busRouteName": "752", - "routeType": "간선", - "startStation": "구산동", - "endStation": "노량진" - }, - { - "routeID": 100100118, - "busRouteName": "753", - "routeType": "간선", - "startStation": "구산동", - "endStation": "상도동" - }, - { - "routeID": 100100129, - "busRouteName": "1014", - "routeType": "지선", - "startStation": "성북생태체험관", - "endStation": "종로구민회관숭인동" - }, - { - "routeID": 100100130, - "busRouteName": "1017", - "routeType": "지선", - "startStation": "월계동", - "endStation": "상왕십리" - }, - { - "routeID": 100100131, - "busRouteName": "1020", - "routeType": "지선", - "startStation": "정릉", - "endStation": "교보문고" - }, - { - "routeID": 100100132, - "busRouteName": "1111", - "routeType": "지선", - "startStation": "번동", - "endStation": "성북동" - }, - { - "routeID": 100100133, - "busRouteName": "1113", - "routeType": "지선", - "startStation": "정릉", - "endStation": "월곡동" - }, - { - "routeID": 100100134, - "busRouteName": "1114", - "routeType": "지선", - "startStation": "성북생태체험관", - "endStation": "길음역" - }, - { - "routeID": 100100137, - "busRouteName": "1119", - "routeType": "지선", - "startStation": "강북청소년수련관", - "endStation": "녹천역" - }, - { - "routeID": 100100138, - "busRouteName": "1120", - "routeType": "지선", - "startStation": "하계동", - "endStation": "삼양동입구" - }, - { - "routeID": 100100139, - "busRouteName": "1124", - "routeType": "지선", - "startStation": "수유역", - "endStation": "미아삼거리역" - }, - { - "routeID": 100100142, - "busRouteName": "1126", - "routeType": "지선", - "startStation": "강북청소년수련관", - "endStation": "안방학동" - }, - { - "routeID": 100100143, - "busRouteName": "1127", - "routeType": "지선", - "startStation": "수유역", - "endStation": "도봉산" - }, - { - "routeID": 100100144, - "busRouteName": "1128", - "routeType": "지선", - "startStation": "도봉산", - "endStation": "월곡동" - }, - { - "routeID": 100100145, - "busRouteName": "1129", - "routeType": "지선", - "startStation": "상계8동", - "endStation": "창동역" - }, - { - "routeID": 100100146, - "busRouteName": "1130", - "routeType": "지선", - "startStation": "청백아파트1단지", - "endStation": "석계역" - }, - { - "routeID": 100100147, - "busRouteName": "1131", - "routeType": "지선", - "startStation": "중계본동", - "endStation": "석계역" - }, - { - "routeID": 100100148, - "busRouteName": "1132", - "routeType": "지선", - "startStation": "월계동", - "endStation": "노원역" - }, - { - "routeID": 100100149, - "busRouteName": "1133", - "routeType": "지선", - "startStation": "염광고교", - "endStation": "염광고교" - }, - { - "routeID": 100100150, - "busRouteName": "1135", - "routeType": "지선", - "startStation": "월계동", - "endStation": "은행사거리" - }, - { - "routeID": 100100151, - "busRouteName": "1136", - "routeType": "지선", - "startStation": "월계동(E마트)", - "endStation": "원자력병원" - }, - { - "routeID": 100100152, - "busRouteName": "1137", - "routeType": "지선", - "startStation": "상계동", - "endStation": "미아사거리" - }, - { - "routeID": 100100153, - "busRouteName": "1138", - "routeType": "지선", - "startStation": "상계4동", - "endStation": "수유역" - }, - { - "routeID": 100100154, - "busRouteName": "1139", - "routeType": "지선", - "startStation": "상계4동", - "endStation": "방학2동주민센터" - }, - { - "routeID": 100100155, - "busRouteName": "1140", - "routeType": "지선", - "startStation": "중계동", - "endStation": "광운대" - }, - { - "routeID": 100100156, - "busRouteName": "1141", - "routeType": "지선", - "startStation": "중계본동종점", - "endStation": "석계역" - }, - { - "routeID": 100100157, - "busRouteName": "1142", - "routeType": "지선", - "startStation": "중계본동", - "endStation": "창동역" - }, - { - "routeID": 100100158, - "busRouteName": "1143", - "routeType": "지선", - "startStation": "중계본동", - "endStation": "수락리버시티" - }, - { - "routeID": 100100159, - "busRouteName": "1144", - "routeType": "지선", - "startStation": "하계동", - "endStation": "우이동" - }, - { - "routeID": 100100164, - "busRouteName": "1154", - "routeType": "지선", - "startStation": "하계동", - "endStation": "신곡동" - }, - { - "routeID": 100100165, - "busRouteName": "1155", - "routeType": "지선", - "startStation": "청학리", - "endStation": "석계역" - }, - { - "routeID": 100100166, - "busRouteName": "1156", - "routeType": "지선", - "startStation": "퇴계원", - "endStation": "석계역" - }, - { - "routeID": 100100170, - "busRouteName": "1162", - "routeType": "지선", - "startStation": "성북구민회관", - "endStation": "보문역" - }, - { - "routeID": 100100171, - "busRouteName": "1164", - "routeType": "지선", - "startStation": "서경대본관", - "endStation": "길음전철역" - }, - { - "routeID": 100100172, - "busRouteName": "1165", - "routeType": "지선", - "startStation": "화계사", - "endStation": "미아사거리역" - }, - { - "routeID": 100100175, - "busRouteName": "1213", - "routeType": "지선", - "startStation": "용마문화복지센터", - "endStation": "국민대학교" - }, - { - "routeID": 100100177, - "busRouteName": "1218", - "routeType": "지선", - "startStation": "수유역", - "endStation": "답십리" - }, - { - "routeID": 100100178, - "busRouteName": "1221", - "routeType": "지선", - "startStation": "중계동", - "endStation": "서울의료원" - }, - { - "routeID": 100100179, - "busRouteName": "1222", - "routeType": "지선", - "startStation": "월계동", - "endStation": "고대앞" - }, - { - "routeID": 100100181, - "busRouteName": "1224", - "routeType": "지선", - "startStation": "상계4동", - "endStation": "청량리" - }, - { - "routeID": 100100183, - "busRouteName": "1226", - "routeType": "지선", - "startStation": "한국과학기술원", - "endStation": "경동시장" - }, - { - "routeID": 100100184, - "busRouteName": "1227", - "routeType": "지선", - "startStation": "하계동", - "endStation": "제기동" - }, - { - "routeID": 100100185, - "busRouteName": "1711", - "routeType": "지선", - "startStation": "국민대", - "endStation": "공덕동" - }, - { - "routeID": 100100186, - "busRouteName": "2012", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "동대문역사문화공원" - }, - { - "routeID": 100100188, - "busRouteName": "2013", - "routeType": "지선", - "startStation": "용마문화복지센터", - "endStation": "성동공업고등학교앞" - }, - { - "routeID": 100100189, - "busRouteName": "2014", - "routeType": "지선", - "startStation": "성수동차고지", - "endStation": "동대문역사문화공원" - }, - { - "routeID": 100100190, - "busRouteName": "2015", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "동대문운동장" - }, - { - "routeID": 100100191, - "busRouteName": "2112", - "routeType": "지선", - "startStation": "면목동", - "endStation": "성북동" - }, - { - "routeID": 100100192, - "busRouteName": "2113", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "석계역" - }, - { - "routeID": 100100193, - "busRouteName": "2114", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "태릉시장" - }, - { - "routeID": 100100194, - "busRouteName": "2211", - "routeType": "지선", - "startStation": "면목동", - "endStation": "회기역" - }, - { - "routeID": 100100198, - "busRouteName": "2221", - "routeType": "지선", - "startStation": "자양동", - "endStation": "신설동" - }, - { - "routeID": 100100199, - "busRouteName": "2222", - "routeType": "지선", - "startStation": "자양동", - "endStation": "고대앞" - }, - { - "routeID": 100100201, - "busRouteName": "2224", - "routeType": "지선", - "startStation": "성수동", - "endStation": "강변역" - }, - { - "routeID": 100100202, - "busRouteName": "2227", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "중곡역" - }, - { - "routeID": 100100203, - "busRouteName": "2230", - "routeType": "지선", - "startStation": "면목동", - "endStation": "경동시장" - }, - { - "routeID": 100100204, - "busRouteName": "2233", - "routeType": "지선", - "startStation": "면목동", - "endStation": "옥수동" - }, - { - "routeID": 100100205, - "busRouteName": "2234", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "봉화산역" - }, - { - "routeID": 100100206, - "busRouteName": "2235", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "신이문역" - }, - { - "routeID": 100100209, - "busRouteName": "2412", - "routeType": "지선", - "startStation": "성수동", - "endStation": "세곡동사거리" - }, - { - "routeID": 100100210, - "busRouteName": "2413", - "routeType": "지선", - "startStation": "성수동", - "endStation": "개포동" - }, - { - "routeID": 100100211, - "busRouteName": "2415", - "routeType": "지선", - "startStation": "자양동", - "endStation": "대치동" - }, - { - "routeID": 100100212, - "busRouteName": "3212", - "routeType": "지선", - "startStation": "강동차고지", - "endStation": "강변역" - }, - { - "routeID": 100100213, - "busRouteName": "3214", - "routeType": "지선", - "startStation": "마천동", - "endStation": "강변역" - }, - { - "routeID": 100100215, - "busRouteName": "3216", - "routeType": "지선", - "startStation": "오금동", - "endStation": "경희대입구" - }, - { - "routeID": 100100216, - "busRouteName": "3217", - "routeType": "지선", - "startStation": "송파차고지", - "endStation": "어린이대공원" - }, - { - "routeID": 100100218, - "busRouteName": "3220", - "routeType": "지선", - "startStation": "오금동", - "endStation": "청량리" - }, - { - "routeID": 100100219, - "busRouteName": "3313", - "routeType": "지선", - "startStation": "송파공영차고지", - "endStation": "잠실새내역" - }, - { - "routeID": 100100220, - "busRouteName": "3314", - "routeType": "지선", - "startStation": "장지동공영차고지", - "endStation": "종합운동장" - }, - { - "routeID": 100100221, - "busRouteName": "3315", - "routeType": "지선", - "startStation": "장지동공영차고지", - "endStation": "삼전동사회복지관" - }, - { - "routeID": 100100222, - "busRouteName": "3316", - "routeType": "지선", - "startStation": "마천동", - "endStation": "천호역" - }, - { - "routeID": 100100223, - "busRouteName": "3411", - "routeType": "지선", - "startStation": "강동차고지", - "endStation": "삼성역" - }, - { - "routeID": 100100224, - "busRouteName": "3412", - "routeType": "지선", - "startStation": "강동차고지", - "endStation": "강남역" - }, - { - "routeID": 100100225, - "busRouteName": "3413", - "routeType": "지선", - "startStation": "강동공영차고지", - "endStation": "수서경찰서" - }, - { - "routeID": 100100226, - "busRouteName": "3414", - "routeType": "지선", - "startStation": "오금동", - "endStation": "고속터미널" - }, - { - "routeID": 100100228, - "busRouteName": "3416", - "routeType": "지선", - "startStation": "마천동", - "endStation": "은곡마을입구.리엔파크2단지" - }, - { - "routeID": 100100229, - "busRouteName": "3417", - "routeType": "지선", - "startStation": "장지공영차고지", - "endStation": "삼성역" - }, - { - "routeID": 100100232, - "busRouteName": "3422", - "routeType": "지선", - "startStation": "장지공영차고지", - "endStation": "강남역" - }, - { - "routeID": 100100234, - "busRouteName": "4212", - "routeType": "지선", - "startStation": "전원마을", - "endStation": "중곡역" - }, - { - "routeID": 100100246, - "busRouteName": "4432", - "routeType": "지선", - "startStation": "개포동", - "endStation": "신원동" - }, - { - "routeID": 100100247, - "busRouteName": "4433", - "routeType": "지선", - "startStation": "대치역", - "endStation": "양재역" - }, - { - "routeID": 100100248, - "busRouteName": "5012", - "routeType": "지선", - "startStation": "가산동", - "endStation": "용산역" - }, - { - "routeID": 100100249, - "busRouteName": "5413", - "routeType": "지선", - "startStation": "시흥", - "endStation": "고속터미널" - }, - { - "routeID": 100100250, - "busRouteName": "5511", - "routeType": "지선", - "startStation": "서울대학교", - "endStation": "중앙대학교" - }, - { - "routeID": 100100251, - "busRouteName": "5513", - "routeType": "지선", - "startStation": "서울대학교", - "endStation": "관악드림타운" - }, - { - "routeID": 100100252, - "busRouteName": "5515", - "routeType": "지선", - "startStation": "금호타운아파트", - "endStation": "청림동현대아파트" - }, - { - "routeID": 100100253, - "busRouteName": "5516", - "routeType": "지선", - "startStation": "신림2동차고지", - "endStation": "노량진역" - }, - { - "routeID": 100100254, - "busRouteName": "5517", - "routeType": "지선", - "startStation": "한남운수대학동차고지", - "endStation": "중앙대학교" - }, - { - "routeID": 100100255, - "busRouteName": "5519", - "routeType": "지선", - "startStation": "우방아파트", - "endStation": "용천사" - }, - { - "routeID": 100100259, - "busRouteName": "5523", - "routeType": "지선", - "startStation": "서울대입구역", - "endStation": "난곡종점" - }, - { - "routeID": 100100260, - "busRouteName": "5524", - "routeType": "지선", - "startStation": "난향차고지", - "endStation": "중앙대학교" - }, - { - "routeID": 100100261, - "busRouteName": "5525", - "routeType": "지선", - "startStation": "시흥동", - "endStation": "보라매공원" - }, - { - "routeID": 100100263, - "busRouteName": "5528", - "routeType": "지선", - "startStation": "가산동", - "endStation": "사당역" - }, - { - "routeID": 100100264, - "busRouteName": "5530", - "routeType": "지선", - "startStation": "군포공영차고지", - "endStation": "사당역" - }, - { - "routeID": 100100265, - "busRouteName": "5531", - "routeType": "지선", - "startStation": "군포공영차고지", - "endStation": "노들역" - }, - { - "routeID": 100100266, - "busRouteName": "5634", - "routeType": "지선", - "startStation": "광명공영차고지", - "endStation": "여의도" - }, - { - "routeID": 100100267, - "busRouteName": "5535", - "routeType": "지선", - "startStation": "하안동", - "endStation": "노량진" - }, - { - "routeID": 100100268, - "busRouteName": "5536", - "routeType": "지선", - "startStation": "하안동", - "endStation": "노량진" - }, - { - "routeID": 100100269, - "busRouteName": "5537", - "routeType": "지선", - "startStation": "시흥동", - "endStation": "가산디지털단지역" - }, - { - "routeID": 100100272, - "busRouteName": "5615", - "routeType": "지선", - "startStation": "여의도", - "endStation": "난곡" - }, - { - "routeID": 100100273, - "busRouteName": "5616", - "routeType": "지선", - "startStation": "가산동기점", - "endStation": "영동중학교" - }, - { - "routeID": 100100274, - "busRouteName": "5617", - "routeType": "지선", - "startStation": "시흥", - "endStation": "구로디지털단지역" - }, - { - "routeID": 100100275, - "busRouteName": "5618", - "routeType": "지선", - "startStation": "구로동", - "endStation": "구로동" - }, - { - "routeID": 100100276, - "busRouteName": "5619", - "routeType": "지선", - "startStation": "시흥동", - "endStation": "신도림역" - }, - { - "routeID": 100100277, - "busRouteName": "5620", - "routeType": "지선", - "startStation": "시흥", - "endStation": "선유도역" - }, - { - "routeID": 100100278, - "busRouteName": "5621", - "routeType": "지선", - "startStation": "삼익아파트", - "endStation": "구로디지털단지역" - }, - { - "routeID": 100100279, - "busRouteName": "5623", - "routeType": "지선", - "startStation": "군포공영차고지", - "endStation": "여의도" - }, - { - "routeID": 100100280, - "busRouteName": "5624", - "routeType": "지선", - "startStation": "부곡버스공영차고지", - "endStation": "구로디지털단지역" - }, - { - "routeID": 100100281, - "busRouteName": "5625", - "routeType": "지선", - "startStation": "안양비산동", - "endStation": "영등포역(영등포시장)" - }, - { - "routeID": 100100282, - "busRouteName": "5626", - "routeType": "지선", - "startStation": "안양비산동", - "endStation": "온수동종점" - }, - { - "routeID": 100100283, - "busRouteName": "5627", - "routeType": "지선", - "startStation": "노온사동", - "endStation": "구로디지털단지역" - }, - { - "routeID": 100100284, - "busRouteName": "5630", - "routeType": "지선", - "startStation": "광명공영차고지", - "endStation": "목동역" - }, - { - "routeID": 100100285, - "busRouteName": "5633", - "routeType": "지선", - "startStation": "노온사동", - "endStation": "순복음교회" - }, - { - "routeID": 100100286, - "busRouteName": "5712", - "routeType": "지선", - "startStation": "가산동기점", - "endStation": "홍대입구역" - }, - { - "routeID": 100100287, - "busRouteName": "5713", - "routeType": "지선", - "startStation": "안양비산동", - "endStation": "신촌기차역" - }, - { - "routeID": 100100288, - "busRouteName": "5714", - "routeType": "지선", - "startStation": "광명공영차고지", - "endStation": "이대입구" - }, - { - "routeID": 100100289, - "busRouteName": "6211", - "routeType": "지선", - "startStation": "신월동", - "endStation": "상왕십리" - }, - { - "routeID": 100100290, - "busRouteName": "6411", - "routeType": "지선", - "startStation": "구로동", - "endStation": "개포동" - }, - { - "routeID": 100100291, - "busRouteName": "6511", - "routeType": "지선", - "startStation": "구로동", - "endStation": "서울대" - }, - { - "routeID": 100100292, - "busRouteName": "6512", - "routeType": "지선", - "startStation": "구로동", - "endStation": "서울대" - }, - { - "routeID": 100100293, - "busRouteName": "6513", - "routeType": "지선", - "startStation": "철산동", - "endStation": "서울대" - }, - { - "routeID": 100100294, - "busRouteName": "6514", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "서울대학교" - }, - { - "routeID": 100100295, - "busRouteName": "6611", - "routeType": "지선", - "startStation": "신도림역", - "endStation": "우성아파트" - }, - { - "routeID": 100100297, - "busRouteName": "6614", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "부천옥길지구" - }, - { - "routeID": 100100298, - "busRouteName": "6616", - "routeType": "지선", - "startStation": "철산동", - "endStation": "온수동" - }, - { - "routeID": 100100299, - "busRouteName": "6617", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "목동우성아파트" - }, - { - "routeID": 100100300, - "busRouteName": "6620", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "당산역" - }, - { - "routeID": 100100301, - "busRouteName": "6623", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "여의도" - }, - { - "routeID": 100100302, - "busRouteName": "6624", - "routeType": "지선", - "startStation": "신월동", - "endStation": "이대목동병원" - }, - { - "routeID": 100100303, - "busRouteName": "6625", - "routeType": "지선", - "startStation": "문래동", - "endStation": "화곡역" - }, - { - "routeID": 100100304, - "busRouteName": "6627", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "이대목동병원" - }, - { - "routeID": 100100305, - "busRouteName": "6628", - "routeType": "지선", - "startStation": "외발산동", - "endStation": "여의도" - }, - { - "routeID": 100100306, - "busRouteName": "6629", - "routeType": "지선", - "startStation": "방화동", - "endStation": "영등포역" - }, - { - "routeID": 100100307, - "busRouteName": "6630", - "routeType": "지선", - "startStation": "영인운수차고지", - "endStation": "영등포시장" - }, - { - "routeID": 100100308, - "busRouteName": "6631", - "routeType": "지선", - "startStation": "강서공영차고지.개화역", - "endStation": "영등포시장" - }, - { - "routeID": 100100309, - "busRouteName": "6632", - "routeType": "지선", - "startStation": "강서공영차고지.개화역", - "endStation": "당산역" - }, - { - "routeID": 100100311, - "busRouteName": "6635", - "routeType": "지선", - "startStation": "광명 하안동", - "endStation": "구로공단역" - }, - { - "routeID": 100100312, - "busRouteName": "6637", - "routeType": "지선", - "startStation": "노온사동", - "endStation": "목동" - }, - { - "routeID": 100100313, - "busRouteName": "6638", - "routeType": "지선", - "startStation": "철산동", - "endStation": "오목교" - }, - { - "routeID": 100100316, - "busRouteName": "6642", - "routeType": "지선", - "startStation": "강서공영차고지.개화역", - "endStation": "가양3동도시개발9단지아파트" - }, - { - "routeID": 100100318, - "busRouteName": "6645", - "routeType": "지선", - "startStation": "강서공영차고지.개화역", - "endStation": "강서공영차고지.개화역" - }, - { - "routeID": 100100320, - "busRouteName": "6647", - "routeType": "지선", - "startStation": "강서공영차고지.개화역", - "endStation": "강서공영차고지.개화역" - }, - { - "routeID": 100100322, - "busRouteName": "6654", - "routeType": "지선", - "startStation": "신풍역", - "endStation": "여의도역" - }, - { - "routeID": 100100329, - "busRouteName": "6657", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "강서한강자이아파트" - }, - { - "routeID": 100100330, - "busRouteName": "6712", - "routeType": "지선", - "startStation": "방화동", - "endStation": "서강대학교" - }, - { - "routeID": 100100331, - "busRouteName": "6714", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "이대부고" - }, - { - "routeID": 100100332, - "busRouteName": "6715", - "routeType": "지선", - "startStation": "신월동", - "endStation": "상암동" - }, - { - "routeID": 100100337, - "busRouteName": "7017", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "롯데백화점" - }, - { - "routeID": 100100338, - "busRouteName": "7018", - "routeType": "지선", - "startStation": "북가좌동", - "endStation": "무교동" - }, - { - "routeID": 100100339, - "busRouteName": "7019", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "서소문" - }, - { - "routeID": 100100340, - "busRouteName": "7021", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "을지로입구" - }, - { - "routeID": 100100341, - "busRouteName": "7022", - "routeType": "지선", - "startStation": "구산동", - "endStation": "서울역" - }, - { - "routeID": 100100342, - "busRouteName": "7024", - "routeType": "지선", - "startStation": "봉원사", - "endStation": "서울역" - }, - { - "routeID": 100100344, - "busRouteName": "7211", - "routeType": "지선", - "startStation": "진관공영차고지", - "endStation": "신설동" - }, - { - "routeID": 100100345, - "busRouteName": "7611", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "여의도" - }, - { - "routeID": 100100346, - "busRouteName": "7612", - "routeType": "지선", - "startStation": "홍연2교", - "endStation": "영등포구청역" - }, - { - "routeID": 100100347, - "busRouteName": "7613", - "routeType": "지선", - "startStation": "구산동", - "endStation": "여의도" - }, - { - "routeID": 100100348, - "busRouteName": "7711", - "routeType": "지선", - "startStation": "덕은동종점", - "endStation": "홍대입구역" - }, - { - "routeID": 100100349, - "busRouteName": "7713", - "routeType": "지선", - "startStation": "홍연2교", - "endStation": "홍연2교" - }, - { - "routeID": 100100352, - "busRouteName": "7720", - "routeType": "지선", - "startStation": "구산동", - "endStation": "신촌" - }, - { - "routeID": 100100353, - "busRouteName": "7722", - "routeType": "지선", - "startStation": "진관공영차고지", - "endStation": "녹번역" - }, - { - "routeID": 100100354, - "busRouteName": "7723", - "routeType": "지선", - "startStation": "진관공영차고지", - "endStation": "구파발역" - }, - { - "routeID": 100100357, - "busRouteName": "7726", - "routeType": "지선", - "startStation": "덕은동종점", - "endStation": "모래내삼거리" - }, - { - "routeID": 100100358, - "busRouteName": "7727", - "routeType": "지선", - "startStation": "설문동", - "endStation": "신촌" - }, - { - "routeID": 100100359, - "busRouteName": "7728", - "routeType": "지선", - "startStation": "대화동", - "endStation": "신촌" - }, - { - "routeID": 100100360, - "busRouteName": "7730", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "이북오도청" - }, - { - "routeID": 100100363, - "busRouteName": "7737", - "routeType": "지선", - "startStation": "은평공영차고지", - "endStation": "파크빌아파트" - }, - { - "routeID": 100100364, - "busRouteName": "7738", - "routeType": "지선", - "startStation": "은평공영차고지", - "endStation": "홍제역" - }, - { - "routeID": 100100383, - "busRouteName": "8541", - "routeType": "맞춤", - "startStation": "호압사", - "endStation": "강남역" - }, - { - "routeID": 100100387, - "busRouteName": "8774", - "routeType": "맞춤", - "startStation": "구산동", - "endStation": "서대문구청" - }, - { - "routeID": 100100389, - "busRouteName": "9401", - "routeType": "광역", - "startStation": "구미동차고지", - "endStation": "서울역" - }, - { - "routeID": 100100390, - "busRouteName": "9403", - "routeType": "광역", - "startStation": "구미동차고지", - "endStation": "동대문역사문화공원" - }, - { - "routeID": 100100391, - "busRouteName": "9404", - "routeType": "광역", - "startStation": "분당구미", - "endStation": "신사역" - }, - { - "routeID": 100100392, - "busRouteName": "9408", - "routeType": "광역", - "startStation": "분당구 구미동", - "endStation": "영등포" - }, - { - "routeID": 100100397, - "busRouteName": "9701", - "routeType": "광역", - "startStation": "고양시 가좌동", - "endStation": "롯데영프라자" - }, - { - "routeID": 100100398, - "busRouteName": "9703", - "routeType": "광역", - "startStation": "신성교통차고지", - "endStation": "서울역" - }, - { - "routeID": 100100400, - "busRouteName": "9707", - "routeType": "광역", - "startStation": "고양 가좌동", - "endStation": "영등포역" - }, - { - "routeID": 100100406, - "busRouteName": "9714", - "routeType": "광역", - "startStation": "교하 운정", - "endStation": "서울역" - }, - { - "routeID": 100100407, - "busRouteName": "240", - "routeType": "간선", - "startStation": "중랑공영차고지", - "endStation": "신사역사거리" - }, - { - "routeID": 100100409, - "busRouteName": "421", - "routeType": "간선", - "startStation": "염곡동차고지", - "endStation": "옥수동" - }, - { - "routeID": 100100410, - "busRouteName": "502", - "routeType": "간선", - "startStation": "월암공영차고지", - "endStation": "한국은행" - }, - { - "routeID": 100100425, - "busRouteName": "1122", - "routeType": "지선", - "startStation": "석관동(상진운수종점)", - "endStation": "원자력병원" - }, - { - "routeID": 100100427, - "busRouteName": "4319", - "routeType": "지선", - "startStation": "전원마을", - "endStation": "잠실역" - }, - { - "routeID": 100100437, - "busRouteName": "771", - "routeType": "간선", - "startStation": "대화동종점", - "endStation": "디지털미디어시티역" - }, - { - "routeID": 100100438, - "busRouteName": "3011", - "routeType": "지선", - "startStation": "장지공영차고지", - "endStation": "한남동" - }, - { - "routeID": 100100440, - "busRouteName": "700", - "routeType": "간선", - "startStation": "대화동", - "endStation": "서울역" - }, - { - "routeID": 100100446, - "busRouteName": "7025", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "종로6가" - }, - { - "routeID": 100100447, - "busRouteName": "7016", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "상명대" - }, - { - "routeID": 100100448, - "busRouteName": "7013B", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "남대문시장" - }, - { - "routeID": 100100449, - "busRouteName": "7013A", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "남대문시장" - }, - { - "routeID": 100100450, - "busRouteName": "7011", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "중구청" - }, - { - "routeID": 100100451, - "busRouteName": "6716", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "이대입구" - }, - { - "routeID": 100100453, - "busRouteName": "6613", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "대림역" - }, - { - "routeID": 100100454, - "busRouteName": "4419", - "routeType": "지선", - "startStation": "송파차고지", - "endStation": "압구정역3번출구" - }, - { - "routeID": 100100459, - "busRouteName": "440", - "routeType": "간선", - "startStation": "송파공영차고지", - "endStation": "압구정동한양파출소앞" - }, - { - "routeID": 100100462, - "busRouteName": "7715", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "연신내역" - }, - { - "routeID": 100100478, - "busRouteName": "3317", - "routeType": "지선", - "startStation": "남한산성입구", - "endStation": "잠실리센츠아파트앞" - }, - { - "routeID": 100100495, - "busRouteName": "672", - "routeType": "간선", - "startStation": "방화동기점", - "endStation": "이대후문" - }, - { - "routeID": 100100496, - "busRouteName": "333", - "routeType": "간선", - "startStation": "송파공영차고지", - "endStation": "올림픽공원" - }, - { - "routeID": 100100497, - "busRouteName": "653", - "routeType": "간선", - "startStation": "영인운수차고지", - "endStation": "가산디지털단지역" - }, - { - "routeID": 100100498, - "busRouteName": "3318", - "routeType": "지선", - "startStation": "강동공영차고지", - "endStation": "마천동" - }, - { - "routeID": 100100499, - "busRouteName": "7212", - "routeType": "지선", - "startStation": "은평차고지", - "endStation": "극동그린아파트앞" - }, - { - "routeID": 100100500, - "busRouteName": "4312", - "routeType": "지선", - "startStation": "개포동", - "endStation": "가락1동주민센터" - }, - { - "routeID": 100100501, - "busRouteName": "3319", - "routeType": "지선", - "startStation": "장지공영차고지", - "endStation": "잠실역7번출구" - }, - { - "routeID": 100100511, - "busRouteName": "707", - "routeType": "간선", - "startStation": "대화동", - "endStation": "서울역" - }, - { - "routeID": 100100521, - "busRouteName": "6515", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "경인교육대학교" - }, - { - "routeID": 100100522, - "busRouteName": "2016", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "효창공원후문" - }, - { - "routeID": 100100525, - "busRouteName": "8772", - "routeType": "맞춤", - "startStation": "진관공영차고지", - "endStation": "북한산성입구" - }, - { - "routeID": 100100537, - "busRouteName": "740", - "routeType": "간선", - "startStation": "덕은동종점", - "endStation": "무역센터삼성역" - }, - { - "routeID": 100100549, - "busRouteName": "100", - "routeType": "간선", - "startStation": "하계동", - "endStation": "용산구청" - }, - { - "routeID": 100100550, - "busRouteName": "662", - "routeType": "간선", - "startStation": "외발산동", - "endStation": "여의나루역" - }, - { - "routeID": 100100551, - "busRouteName": "673", - "routeType": "간선", - "startStation": "부천상동", - "endStation": "이대부고" - }, - { - "routeID": 100100552, - "busRouteName": "7739", - "routeType": "지선", - "startStation": "은평공영차고지", - "endStation": "서교가든" - }, - { - "routeID": 100100553, - "busRouteName": "350", - "routeType": "간선", - "startStation": "송파차고지", - "endStation": "노들역앞" - }, - { - "routeID": 100100564, - "busRouteName": "320", - "routeType": "간선", - "startStation": "송파차고지", - "endStation": "중랑구청" - }, - { - "routeID": 100100565, - "busRouteName": "750B", - "routeType": "간선", - "startStation": "은평차고지", - "endStation": "서울대학교" - }, - { - "routeID": 100100566, - "busRouteName": "1115", - "routeType": "지선", - "startStation": "수유중학교혜화여고", - "endStation": "미아삼거리역" - }, - { - "routeID": 100100574, - "busRouteName": "541", - "routeType": "간선", - "startStation": "군포공영차고지", - "endStation": "강남역" - }, - { - "routeID": 100100576, - "busRouteName": "6516", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "박미삼거리" - }, - { - "routeID": 100100578, - "busRouteName": "3321", - "routeType": "지선", - "startStation": "강동공영차고지", - "endStation": "강동구청" - }, - { - "routeID": 100100579, - "busRouteName": "3012", - "routeType": "지선", - "startStation": "송파차고지", - "endStation": "이촌2동" - }, - { - "routeID": 100100583, - "busRouteName": "121", - "routeType": "간선", - "startStation": "화계사", - "endStation": "서울숲" - }, - { - "routeID": 100100587, - "busRouteName": "705", - "routeType": "간선", - "startStation": "은평뉴타운공영차고지", - "endStation": "롯데백화점" - }, - { - "routeID": 100100591, - "busRouteName": "N30", - "routeType": "심야", - "startStation": "강동공영차고지", - "endStation": "서울역환승센터" - }, - { - "routeID": 100100595, - "busRouteName": "241", - "routeType": "간선", - "startStation": "중랑공영차고지", - "endStation": "논현역" - }, - { - "routeID": 100100596, - "busRouteName": "400", - "routeType": "간선", - "startStation": "염곡동", - "endStation": "시청앞" - }, - { - "routeID": 100100597, - "busRouteName": "405", - "routeType": "간선", - "startStation": "염곡동", - "endStation": "시청광장" - }, - { - "routeID": 100100598, - "busRouteName": "2115", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "서경대입구" - }, - { - "routeID": 100100599, - "busRouteName": "2311", - "routeType": "지선", - "startStation": "중랑공영차고지", - "endStation": "문정동" - }, - { - "routeID": 100100601, - "busRouteName": "6640A", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "양천공영차고지" - }, - { - "routeID": 100100602, - "busRouteName": "6640B", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "양천공영차고지" - }, - { - "routeID": 100100603, - "busRouteName": "542", - "routeType": "간선", - "startStation": "군포복합화물터미널", - "endStation": "신사역" - }, - { - "routeID": 100100604, - "busRouteName": "4211", - "routeType": "지선", - "startStation": "염곡동차고지", - "endStation": "한양대동문앞" - }, - { - "routeID": 100100605, - "busRouteName": "463", - "routeType": "간선", - "startStation": "염곡동차고지", - "endStation": "국회의사당" - }, - { - "routeID": 100100607, - "busRouteName": "9711", - "routeType": "광역", - "startStation": "신성교통차고지", - "endStation": "양재동" - }, - { - "routeID": 100100609, - "busRouteName": "3425", - "routeType": "지선", - "startStation": "송파공영차고지", - "endStation": "삼성역" - }, - { - "routeID": 100100612, - "busRouteName": "3426", - "routeType": "지선", - "startStation": "서울버스종점", - "endStation": "청담동" - }, - { - "routeID": 100100613, - "busRouteName": "3322", - "routeType": "지선", - "startStation": "송파차고지", - "endStation": "잠실종합운동장" - }, - { - "routeID": 103000001, - "busRouteName": "8441", - "routeType": "맞춤", - "startStation": "은곡마을", - "endStation": "LH이편한세상" - }, - { - "routeID": 104000006, - "busRouteName": "242", - "routeType": "간선", - "startStation": "중랑공영차고지", - "endStation": "개포동" - }, - { - "routeID": 104000007, - "busRouteName": "01A", - "routeType": "순환", - "startStation": "서울역버스환승센터", - "endStation": "서울역버스환승센터" - }, - { - "routeID": 104000008, - "busRouteName": "01B", - "routeType": "순환", - "startStation": "서울역버스환승센터", - "endStation": "서울역버스환승센터" - }, - { - "routeID": 106000001, - "busRouteName": "8221", - "routeType": "맞춤", - "startStation": "장안2동주민센터", - "endStation": "답십리역" - }, - { - "routeID": 107000001, - "busRouteName": "1116", - "routeType": "지선", - "startStation": "국민대학교", - "endStation": "미아사거리" - }, - { - "routeID": 107000002, - "busRouteName": "343", - "routeType": "간선", - "startStation": "송파공영차고지", - "endStation": "수서역" - }, - { - "routeID": 107000003, - "busRouteName": "8002", - "routeType": "지선", - "startStation": "상명대앞", - "endStation": "경복궁역" - }, - { - "routeID": 107000004, - "busRouteName": "8003", - "routeType": "지선", - "startStation": "평창동주민센터", - "endStation": "평창동주민센터" - }, - { - "routeID": 108000001, - "busRouteName": "8551", - "routeType": "맞춤", - "startStation": "봉천역", - "endStation": "노량진역" - }, - { - "routeID": 108000002, - "busRouteName": "1167", - "routeType": "지선", - "startStation": "우이동", - "endStation": "은행사거리(11414)" - }, - { - "routeID": 110000002, - "busRouteName": "173", - "routeType": "간선", - "startStation": "월계동", - "endStation": "신촌기차역" - }, - { - "routeID": 110000003, - "busRouteName": "8112", - "routeType": "맞춤", - "startStation": "장위뉴타운", - "endStation": "성신여대입구" - }, - { - "routeID": 111000008, - "busRouteName": "674", - "routeType": "간선", - "startStation": "신길운수", - "endStation": "연세대" - }, - { - "routeID": 111000009, - "busRouteName": "8771", - "routeType": "맞춤", - "startStation": "구산중", - "endStation": "구산교회" - }, - { - "routeID": 111000010, - "busRouteName": "7734", - "routeType": "지선", - "startStation": "진관공영차고지", - "endStation": "홍대입구역" - }, - { - "routeID": 111000011, - "busRouteName": "773", - "routeType": "간선", - "startStation": "교하운정", - "endStation": "불광역" - }, - { - "routeID": 111000012, - "busRouteName": "774", - "routeType": "간선", - "startStation": "진관공영차고지", - "endStation": "파주읍" - }, - { - "routeID": 111000014, - "busRouteName": "761", - "routeType": "간선", - "startStation": "진관공영차고지", - "endStation": "영등포역" - }, - { - "routeID": 112000001, - "busRouteName": "8777", - "routeType": "맞춤", - "startStation": "난지캠핑장", - "endStation": "월드컵경기장남측" - }, - { - "routeID": 113000001, - "busRouteName": "8761", - "routeType": "맞춤", - "startStation": "신촌로터리", - "endStation": "국회의사당" - }, - { - "routeID": 113000002, - "busRouteName": "452", - "routeType": "간선", - "startStation": "송파공영차고지", - "endStation": "중앙대학교" - }, - { - "routeID": 114000001, - "busRouteName": "6649", - "routeType": "지선", - "startStation": "신도림역", - "endStation": "오목교역청학스포츠타운" - }, - { - "routeID": 115000005, - "busRouteName": "6648", - "routeType": "지선", - "startStation": "방화동", - "endStation": "양천구청" - }, - { - "routeID": 115000007, - "busRouteName": "654", - "routeType": "간선", - "startStation": "방화동", - "endStation": "노들역" - }, - { - "routeID": 116000001, - "busRouteName": "8552", - "routeType": "맞춤", - "startStation": "신림복지관앞", - "endStation": "신림역신림사거리" - }, - { - "routeID": 116000002, - "busRouteName": "6615", - "routeType": "지선", - "startStation": "양천공영차고지", - "endStation": "천왕역" - }, - { - "routeID": 116000004, - "busRouteName": "660", - "routeType": "간선", - "startStation": "온수동종점", - "endStation": "가양역" - }, - { - "routeID": 122000001, - "busRouteName": "4435", - "routeType": "지선", - "startStation": "개포동차고지", - "endStation": "사당역" - }, - { - "routeID": 123000010, - "busRouteName": "741", - "routeType": "간선", - "startStation": "진관차고지", - "endStation": "세곡동사거리" - }, - { - "routeID": 123000011, - "busRouteName": "708", - "routeType": "간선", - "startStation": "진관차고지", - "endStation": "서울역" - }, - { - "routeID": 124000008, - "busRouteName": "7719", - "routeType": "지선", - "startStation": "북가좌동", - "endStation": "녹번동" - }, - { - "routeID": 124000010, - "busRouteName": "8331", - "routeType": "맞춤", - "startStation": "마천사거리", - "endStation": "잠실역" - }, - { - "routeID": 124000036, - "busRouteName": "362", - "routeType": "간선", - "startStation": "송파공영차고지", - "endStation": "여의도" - }, - { - "routeID": 124000038, - "busRouteName": "342", - "routeType": "간선", - "startStation": "강동차고지", - "endStation": "압구정로데오역" - } + { + "routeID": 100100006, + "busRouteName": "101", + "routeType": "간선", + "startStation": "우이동", + "endStation": "서소문" + }, + { + "routeID": 100100007, + "busRouteName": "102", + "routeType": "간선", + "startStation": "상계주공7단지", + "endStation": "동대문" + }, + { + "routeID": 100100008, + "busRouteName": "103", + "routeType": "간선", + "startStation": "삼화상운", + "endStation": "서울역" + }, + { + "routeID": 100100009, + "busRouteName": "104", + "routeType": "간선", + "startStation": "강북청소년수련관", + "endStation": "서울역버스환승센터" + }, + { + "routeID": 100100010, + "busRouteName": "105", + "routeType": "간선", + "startStation": "상계주공7단지", + "endStation": "서울역" + }, + { + "routeID": 100100011, + "busRouteName": "106", + "routeType": "간선", + "startStation": "의정부", + "endStation": "종로5가" + }, + { + "routeID": 100100012, + "busRouteName": "107", + "routeType": "간선", + "startStation": "민락동차고지", + "endStation": "동대문" + }, + { + "routeID": 100100014, + "busRouteName": "109", + "routeType": "간선", + "startStation": "우이동", + "endStation": "광화문" + }, + { + "routeID": 100100017, + "busRouteName": "120", + "routeType": "간선", + "startStation": "우이동", + "endStation": "청량리" + }, + { + "routeID": 100100018, + "busRouteName": "130", + "routeType": "간선", + "startStation": "우이동", + "endStation": "길동" + }, + { + "routeID": 100100019, + "busRouteName": "140", + "routeType": "간선", + "startStation": "도봉산역광역환승센터", + "endStation": "AT센터양재꽃시장" + }, + { + "routeID": 100100020, + "busRouteName": "141", + "routeType": "간선", + "startStation": "도봉산", + "endStation": "염곡동" + }, + { + "routeID": 100100021, + "busRouteName": "142", + "routeType": "간선", + "startStation": "방배동", + "endStation": "도봉산" + }, + { + "routeID": 100100022, + "busRouteName": "143", + "routeType": "간선", + "startStation": "정릉", + "endStation": "개포동" + }, + { + "routeID": 100100023, + "busRouteName": "144", + "routeType": "간선", + "startStation": "우이동", + "endStation": "교대" + }, + { + "routeID": 100100024, + "busRouteName": "145", + "routeType": "간선", + "startStation": "번동", + "endStation": "강남역" + }, + { + "routeID": 100100025, + "busRouteName": "146", + "routeType": "간선", + "startStation": "상계주공7단지", + "endStation": "강남역" + }, + { + "routeID": 100100026, + "busRouteName": "147", + "routeType": "간선", + "startStation": "월계동", + "endStation": "도곡동" + }, + { + "routeID": 100100027, + "busRouteName": "148", + "routeType": "간선", + "startStation": "번동", + "endStation": "방배동" + }, + { + "routeID": 100100029, + "busRouteName": "150", + "routeType": "간선", + "startStation": "도봉산역", + "endStation": "시흥대교" + }, + { + "routeID": 100100030, + "busRouteName": "151", + "routeType": "간선", + "startStation": "우이동", + "endStation": "중앙대" + }, + { + "routeID": 100100031, + "busRouteName": "152", + "routeType": "간선", + "startStation": "화계사", + "endStation": "삼막사사거리" + }, + { + "routeID": 100100032, + "busRouteName": "153", + "routeType": "간선", + "startStation": "우이동", + "endStation": "당곡사거리" + }, + { + "routeID": 100100033, + "busRouteName": "160", + "routeType": "간선", + "startStation": "도봉산역", + "endStation": "온수동종점" + }, + { + "routeID": 100100034, + "busRouteName": "162", + "routeType": "간선", + "startStation": "정릉", + "endStation": "여의도" + }, + { + "routeID": 100100036, + "busRouteName": "171", + "routeType": "간선", + "startStation": "국민대앞", + "endStation": "월드컵파크7단지" + }, + { + "routeID": 100100037, + "busRouteName": "172", + "routeType": "간선", + "startStation": "하계동", + "endStation": "상암동" + }, + { + "routeID": 100100038, + "busRouteName": "201", + "routeType": "간선", + "startStation": "수택동차고지", + "endStation": "서울역환승센터" + }, + { + "routeID": 100100039, + "busRouteName": "202", + "routeType": "간선", + "startStation": "불암동", + "endStation": "후암동" + }, + { + "routeID": 100100042, + "busRouteName": "260", + "routeType": "간선", + "startStation": "중랑공영차고지", + "endStation": "국회의사당" + }, + { + "routeID": 100100043, + "busRouteName": "261", + "routeType": "간선", + "startStation": "석관동(상진운수종점)", + "endStation": "여의도" + }, + { + "routeID": 100100044, + "busRouteName": "262", + "routeType": "간선", + "startStation": "중랑공영차고지", + "endStation": "여의도환승센타" + }, + { + "routeID": 100100046, + "busRouteName": "270", + "routeType": "간선", + "startStation": "상암차고지", + "endStation": "망우리(출)" + }, + { + "routeID": 100100047, + "busRouteName": "271", + "routeType": "간선", + "startStation": "용마문화복지센터", + "endStation": "월드컵파크7단지" + }, + { + "routeID": 100100048, + "busRouteName": "272", + "routeType": "간선", + "startStation": "면목동", + "endStation": "남가좌동" + }, + { + "routeID": 100100049, + "busRouteName": "273", + "routeType": "간선", + "startStation": "중랑공영차고지", + "endStation": "홍대입구역" + }, + { + "routeID": 100100051, + "busRouteName": "301", + "routeType": "간선", + "startStation": "장지공영차고지", + "endStation": "혜화동" + }, + { + "routeID": 100100052, + "busRouteName": "302", + "routeType": "간선", + "startStation": "상대원차고지", + "endStation": "상왕십리역" + }, + { + "routeID": 100100053, + "busRouteName": "303", + "routeType": "간선", + "startStation": "상대원차고지", + "endStation": "신설동역" + }, + { + "routeID": 100100055, + "busRouteName": "340", + "routeType": "간선", + "startStation": "강동공영차고지", + "endStation": "강남역" + }, + { + "routeID": 100100056, + "busRouteName": "341", + "routeType": "간선", + "startStation": "하남공영차고지", + "endStation": "강남역" + }, + { + "routeID": 100100057, + "busRouteName": "360", + "routeType": "간선", + "startStation": "송파차고지", + "endStation": "여의도환승센터" + }, + { + "routeID": 100100061, + "busRouteName": "370", + "routeType": "간선", + "startStation": "강동공영차고지", + "endStation": "충정로역" + }, + { + "routeID": 100100062, + "busRouteName": "401", + "routeType": "간선", + "startStation": "장지공영차고지", + "endStation": "서울역" + }, + { + "routeID": 100100063, + "busRouteName": "402", + "routeType": "간선", + "startStation": "장지공영차고지", + "endStation": "광화문" + }, + { + "routeID": 100100064, + "busRouteName": "406", + "routeType": "간선", + "startStation": "개포동", + "endStation": "서울역" + }, + { + "routeID": 100100068, + "busRouteName": "420", + "routeType": "간선", + "startStation": "개포동", + "endStation": "청량리" + }, + { + "routeID": 100100070, + "busRouteName": "441", + "routeType": "간선", + "startStation": "월암공영차고지", + "endStation": "신사역" + }, + { + "routeID": 100100071, + "busRouteName": "461", + "routeType": "간선", + "startStation": "장지공영차고지", + "endStation": "여의도" + }, + { + "routeID": 100100073, + "busRouteName": "470", + "routeType": "간선", + "startStation": "상암차고지", + "endStation": "안골마을" + }, + { + "routeID": 100100075, + "busRouteName": "472", + "routeType": "간선", + "startStation": "개포동차고지", + "endStation": "신촌로타리" + }, + { + "routeID": 100100076, + "busRouteName": "500", + "routeType": "간선", + "startStation": "석수역", + "endStation": "을지로입구" + }, + { + "routeID": 100100077, + "busRouteName": "501", + "routeType": "간선", + "startStation": "서울대학교", + "endStation": "종로2가" + }, + { + "routeID": 100100078, + "busRouteName": "503", + "routeType": "간선", + "startStation": "광명공영차고지", + "endStation": "서울역" + }, + { + "routeID": 100100079, + "busRouteName": "504", + "routeType": "간선", + "startStation": "광명공영주차장", + "endStation": "남대문" + }, + { + "routeID": 100100080, + "busRouteName": "505", + "routeType": "간선", + "startStation": "노온사동", + "endStation": "서울역" + }, + { + "routeID": 100100081, + "busRouteName": "506", + "routeType": "간선", + "startStation": "신림2동차고지", + "endStation": "을지로입구역광교" + }, + { + "routeID": 100100082, + "busRouteName": "507", + "routeType": "간선", + "startStation": "석수역", + "endStation": "동대문역사문화공원" + }, + { + "routeID": 100100083, + "busRouteName": "540", + "routeType": "간선", + "startStation": "군포공영차고지", + "endStation": "서울성모병원" + }, + { + "routeID": 100100084, + "busRouteName": "571", + "routeType": "간선", + "startStation": "가산동", + "endStation": "은평뉴타운공영차고지" + }, + { + "routeID": 100100085, + "busRouteName": "600", + "routeType": "간선", + "startStation": "온수동", + "endStation": "광화문" + }, + { + "routeID": 100100086, + "busRouteName": "601", + "routeType": "간선", + "startStation": "개화동", + "endStation": "혜화역" + }, + { + "routeID": 100100087, + "busRouteName": "602", + "routeType": "간선", + "startStation": "양천공영차고지", + "endStation": "시청앞" + }, + { + "routeID": 100100088, + "busRouteName": "603", + "routeType": "간선", + "startStation": "신월동", + "endStation": "시청" + }, + { + "routeID": 100100089, + "busRouteName": "604", + "routeType": "간선", + "startStation": "신월동기점", + "endStation": "중구청앞" + }, + { + "routeID": 100100090, + "busRouteName": "605", + "routeType": "간선", + "startStation": "강서공영차고지.개화역", + "endStation": "후암동" + }, + { + "routeID": 100100091, + "busRouteName": "606", + "routeType": "간선", + "startStation": "부천상동", + "endStation": "조계사" + }, + { + "routeID": 100100093, + "busRouteName": "640", + "routeType": "간선", + "startStation": "신월동기점", + "endStation": "강남역" + }, + { + "routeID": 100100094, + "busRouteName": "641", + "routeType": "간선", + "startStation": "문래동", + "endStation": "양재동" + }, + { + "routeID": 100100096, + "busRouteName": "643", + "routeType": "간선", + "startStation": "양천차고지", + "endStation": "강남역" + }, + { + "routeID": 100100097, + "busRouteName": "650", + "routeType": "간선", + "startStation": "외발산동", + "endStation": "낙성대입구" + }, + { + "routeID": 100100098, + "busRouteName": "651", + "routeType": "간선", + "startStation": "방화동", + "endStation": "관악구청" + }, + { + "routeID": 100100099, + "busRouteName": "652", + "routeType": "간선", + "startStation": "신월동기점", + "endStation": "금천우체국독산1동주민센터" + }, + { + "routeID": 100100102, + "busRouteName": "661", + "routeType": "간선", + "startStation": "부천상동", + "endStation": "신세계백화점" + }, + { + "routeID": 100100103, + "busRouteName": "701", + "routeType": "간선", + "startStation": "진관차고지", + "endStation": "종로2가삼일교" + }, + { + "routeID": 100100107, + "busRouteName": "704", + "routeType": "간선", + "startStation": "송추", + "endStation": "서울역버스환승센터" + }, + { + "routeID": 100100110, + "busRouteName": "710", + "routeType": "간선", + "startStation": "상암차고지", + "endStation": "수유역강북구청" + }, + { + "routeID": 100100111, + "busRouteName": "720", + "routeType": "간선", + "startStation": "진관공영차고지", + "endStation": "답십리" + }, + { + "routeID": 100100112, + "busRouteName": "721", + "routeType": "간선", + "startStation": "북가좌동", + "endStation": "건대입구역" + }, + { + "routeID": 100100114, + "busRouteName": "750A", + "routeType": "간선", + "startStation": "덕은동종점", + "endStation": "서울대학교" + }, + { + "routeID": 100100116, + "busRouteName": "742", + "routeType": "간선", + "startStation": "구산동", + "endStation": "상도동" + }, + { + "routeID": 100100117, + "busRouteName": "752", + "routeType": "간선", + "startStation": "구산동", + "endStation": "노량진" + }, + { + "routeID": 100100118, + "busRouteName": "753", + "routeType": "간선", + "startStation": "구산동", + "endStation": "상도동" + }, + { + "routeID": 100100129, + "busRouteName": "1014", + "routeType": "지선", + "startStation": "성북생태체험관", + "endStation": "종로구민회관숭인동" + }, + { + "routeID": 100100130, + "busRouteName": "1017", + "routeType": "지선", + "startStation": "월계동", + "endStation": "상왕십리" + }, + { + "routeID": 100100131, + "busRouteName": "1020", + "routeType": "지선", + "startStation": "정릉", + "endStation": "교보문고" + }, + { + "routeID": 100100132, + "busRouteName": "1111", + "routeType": "지선", + "startStation": "번동", + "endStation": "성북동" + }, + { + "routeID": 100100133, + "busRouteName": "1113", + "routeType": "지선", + "startStation": "정릉", + "endStation": "월곡동" + }, + { + "routeID": 100100134, + "busRouteName": "1114", + "routeType": "지선", + "startStation": "성북생태체험관", + "endStation": "길음역" + }, + { + "routeID": 100100137, + "busRouteName": "1119", + "routeType": "지선", + "startStation": "강북청소년수련관", + "endStation": "녹천역" + }, + { + "routeID": 100100138, + "busRouteName": "1120", + "routeType": "지선", + "startStation": "하계동", + "endStation": "삼양동입구" + }, + { + "routeID": 100100139, + "busRouteName": "1124", + "routeType": "지선", + "startStation": "수유역", + "endStation": "미아삼거리역" + }, + { + "routeID": 100100142, + "busRouteName": "1126", + "routeType": "지선", + "startStation": "강북청소년수련관", + "endStation": "안방학동" + }, + { + "routeID": 100100143, + "busRouteName": "1127", + "routeType": "지선", + "startStation": "수유역", + "endStation": "도봉산" + }, + { + "routeID": 100100144, + "busRouteName": "1128", + "routeType": "지선", + "startStation": "도봉산", + "endStation": "월곡동" + }, + { + "routeID": 100100145, + "busRouteName": "1129", + "routeType": "지선", + "startStation": "상계8동", + "endStation": "창동역" + }, + { + "routeID": 100100146, + "busRouteName": "1130", + "routeType": "지선", + "startStation": "청백아파트1단지", + "endStation": "석계역" + }, + { + "routeID": 100100147, + "busRouteName": "1131", + "routeType": "지선", + "startStation": "중계본동", + "endStation": "석계역" + }, + { + "routeID": 100100148, + "busRouteName": "1132", + "routeType": "지선", + "startStation": "월계동", + "endStation": "노원역" + }, + { + "routeID": 100100149, + "busRouteName": "1133", + "routeType": "지선", + "startStation": "염광고교", + "endStation": "염광고교" + }, + { + "routeID": 100100150, + "busRouteName": "1135", + "routeType": "지선", + "startStation": "월계동", + "endStation": "은행사거리" + }, + { + "routeID": 100100151, + "busRouteName": "1136", + "routeType": "지선", + "startStation": "월계동(E마트)", + "endStation": "원자력병원" + }, + { + "routeID": 100100152, + "busRouteName": "1137", + "routeType": "지선", + "startStation": "상계동", + "endStation": "미아사거리" + }, + { + "routeID": 100100153, + "busRouteName": "1138", + "routeType": "지선", + "startStation": "상계4동", + "endStation": "수유역" + }, + { + "routeID": 100100154, + "busRouteName": "1139", + "routeType": "지선", + "startStation": "상계4동", + "endStation": "방학2동주민센터" + }, + { + "routeID": 100100155, + "busRouteName": "1140", + "routeType": "지선", + "startStation": "중계동", + "endStation": "광운대" + }, + { + "routeID": 100100156, + "busRouteName": "1141", + "routeType": "지선", + "startStation": "중계본동종점", + "endStation": "석계역" + }, + { + "routeID": 100100157, + "busRouteName": "1142", + "routeType": "지선", + "startStation": "중계본동", + "endStation": "창동역" + }, + { + "routeID": 100100158, + "busRouteName": "1143", + "routeType": "지선", + "startStation": "중계본동", + "endStation": "수락리버시티" + }, + { + "routeID": 100100159, + "busRouteName": "1144", + "routeType": "지선", + "startStation": "하계동", + "endStation": "우이동" + }, + { + "routeID": 100100164, + "busRouteName": "1154", + "routeType": "지선", + "startStation": "하계동", + "endStation": "신곡동" + }, + { + "routeID": 100100165, + "busRouteName": "1155", + "routeType": "지선", + "startStation": "청학리", + "endStation": "석계역" + }, + { + "routeID": 100100166, + "busRouteName": "1156", + "routeType": "지선", + "startStation": "퇴계원", + "endStation": "석계역" + }, + { + "routeID": 100100170, + "busRouteName": "1162", + "routeType": "지선", + "startStation": "성북구민회관", + "endStation": "보문역" + }, + { + "routeID": 100100171, + "busRouteName": "1164", + "routeType": "지선", + "startStation": "서경대본관", + "endStation": "길음전철역" + }, + { + "routeID": 100100172, + "busRouteName": "1165", + "routeType": "지선", + "startStation": "화계사", + "endStation": "미아사거리역" + }, + { + "routeID": 100100175, + "busRouteName": "1213", + "routeType": "지선", + "startStation": "용마문화복지센터", + "endStation": "국민대학교" + }, + { + "routeID": 100100177, + "busRouteName": "1218", + "routeType": "지선", + "startStation": "수유역", + "endStation": "답십리" + }, + { + "routeID": 100100178, + "busRouteName": "1221", + "routeType": "지선", + "startStation": "중계동", + "endStation": "서울의료원" + }, + { + "routeID": 100100179, + "busRouteName": "1222", + "routeType": "지선", + "startStation": "월계동", + "endStation": "고대앞" + }, + { + "routeID": 100100181, + "busRouteName": "1224", + "routeType": "지선", + "startStation": "상계4동", + "endStation": "청량리" + }, + { + "routeID": 100100183, + "busRouteName": "1226", + "routeType": "지선", + "startStation": "한국과학기술원", + "endStation": "경동시장" + }, + { + "routeID": 100100184, + "busRouteName": "1227", + "routeType": "지선", + "startStation": "하계동", + "endStation": "제기동" + }, + { + "routeID": 100100185, + "busRouteName": "1711", + "routeType": "지선", + "startStation": "국민대", + "endStation": "공덕동" + }, + { + "routeID": 100100186, + "busRouteName": "2012", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "동대문역사문화공원" + }, + { + "routeID": 100100188, + "busRouteName": "2013", + "routeType": "지선", + "startStation": "용마문화복지센터", + "endStation": "성동공업고등학교앞" + }, + { + "routeID": 100100189, + "busRouteName": "2014", + "routeType": "지선", + "startStation": "성수동차고지", + "endStation": "동대문역사문화공원" + }, + { + "routeID": 100100190, + "busRouteName": "2015", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "동대문운동장" + }, + { + "routeID": 100100191, + "busRouteName": "2112", + "routeType": "지선", + "startStation": "면목동", + "endStation": "성북동" + }, + { + "routeID": 100100192, + "busRouteName": "2113", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "석계역" + }, + { + "routeID": 100100193, + "busRouteName": "2114", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "태릉시장" + }, + { + "routeID": 100100194, + "busRouteName": "2211", + "routeType": "지선", + "startStation": "면목동", + "endStation": "회기역" + }, + { + "routeID": 100100198, + "busRouteName": "2221", + "routeType": "지선", + "startStation": "자양동", + "endStation": "신설동" + }, + { + "routeID": 100100199, + "busRouteName": "2222", + "routeType": "지선", + "startStation": "자양동", + "endStation": "고대앞" + }, + { + "routeID": 100100201, + "busRouteName": "2224", + "routeType": "지선", + "startStation": "성수동", + "endStation": "강변역" + }, + { + "routeID": 100100202, + "busRouteName": "2227", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "중곡역" + }, + { + "routeID": 100100203, + "busRouteName": "2230", + "routeType": "지선", + "startStation": "면목동", + "endStation": "경동시장" + }, + { + "routeID": 100100204, + "busRouteName": "2233", + "routeType": "지선", + "startStation": "면목동", + "endStation": "옥수동" + }, + { + "routeID": 100100205, + "busRouteName": "2234", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "봉화산역" + }, + { + "routeID": 100100206, + "busRouteName": "2235", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "신이문역" + }, + { + "routeID": 100100209, + "busRouteName": "2412", + "routeType": "지선", + "startStation": "성수동", + "endStation": "세곡동사거리" + }, + { + "routeID": 100100210, + "busRouteName": "2413", + "routeType": "지선", + "startStation": "성수동", + "endStation": "개포동" + }, + { + "routeID": 100100211, + "busRouteName": "2415", + "routeType": "지선", + "startStation": "자양동", + "endStation": "대치동" + }, + { + "routeID": 100100212, + "busRouteName": "3212", + "routeType": "지선", + "startStation": "강동차고지", + "endStation": "강변역" + }, + { + "routeID": 100100213, + "busRouteName": "3214", + "routeType": "지선", + "startStation": "마천동", + "endStation": "강변역" + }, + { + "routeID": 100100215, + "busRouteName": "3216", + "routeType": "지선", + "startStation": "오금동", + "endStation": "경희대입구" + }, + { + "routeID": 100100216, + "busRouteName": "3217", + "routeType": "지선", + "startStation": "송파차고지", + "endStation": "어린이대공원" + }, + { + "routeID": 100100218, + "busRouteName": "3220", + "routeType": "지선", + "startStation": "오금동", + "endStation": "청량리" + }, + { + "routeID": 100100219, + "busRouteName": "3313", + "routeType": "지선", + "startStation": "송파공영차고지", + "endStation": "잠실새내역" + }, + { + "routeID": 100100220, + "busRouteName": "3314", + "routeType": "지선", + "startStation": "장지동공영차고지", + "endStation": "종합운동장" + }, + { + "routeID": 100100221, + "busRouteName": "3315", + "routeType": "지선", + "startStation": "장지동공영차고지", + "endStation": "삼전동사회복지관" + }, + { + "routeID": 100100222, + "busRouteName": "3316", + "routeType": "지선", + "startStation": "마천동", + "endStation": "천호역" + }, + { + "routeID": 100100223, + "busRouteName": "3411", + "routeType": "지선", + "startStation": "강동차고지", + "endStation": "삼성역" + }, + { + "routeID": 100100224, + "busRouteName": "3412", + "routeType": "지선", + "startStation": "강동차고지", + "endStation": "강남역" + }, + { + "routeID": 100100225, + "busRouteName": "3413", + "routeType": "지선", + "startStation": "강동공영차고지", + "endStation": "수서경찰서" + }, + { + "routeID": 100100226, + "busRouteName": "3414", + "routeType": "지선", + "startStation": "오금동", + "endStation": "고속터미널" + }, + { + "routeID": 100100228, + "busRouteName": "3416", + "routeType": "지선", + "startStation": "마천동", + "endStation": "은곡마을입구.리엔파크2단지" + }, + { + "routeID": 100100229, + "busRouteName": "3417", + "routeType": "지선", + "startStation": "장지공영차고지", + "endStation": "삼성역" + }, + { + "routeID": 100100232, + "busRouteName": "3422", + "routeType": "지선", + "startStation": "장지공영차고지", + "endStation": "강남역" + }, + { + "routeID": 100100234, + "busRouteName": "4212", + "routeType": "지선", + "startStation": "전원마을", + "endStation": "중곡역" + }, + { + "routeID": 100100246, + "busRouteName": "4432", + "routeType": "지선", + "startStation": "개포동", + "endStation": "신원동" + }, + { + "routeID": 100100247, + "busRouteName": "4433", + "routeType": "지선", + "startStation": "대치역", + "endStation": "양재역" + }, + { + "routeID": 100100248, + "busRouteName": "5012", + "routeType": "지선", + "startStation": "가산동", + "endStation": "용산역" + }, + { + "routeID": 100100249, + "busRouteName": "5413", + "routeType": "지선", + "startStation": "시흥", + "endStation": "고속터미널" + }, + { + "routeID": 100100250, + "busRouteName": "5511", + "routeType": "지선", + "startStation": "서울대학교", + "endStation": "중앙대학교" + }, + { + "routeID": 100100251, + "busRouteName": "5513", + "routeType": "지선", + "startStation": "서울대학교", + "endStation": "관악드림타운" + }, + { + "routeID": 100100252, + "busRouteName": "5515", + "routeType": "지선", + "startStation": "금호타운아파트", + "endStation": "청림동현대아파트" + }, + { + "routeID": 100100253, + "busRouteName": "5516", + "routeType": "지선", + "startStation": "신림2동차고지", + "endStation": "노량진역" + }, + { + "routeID": 100100254, + "busRouteName": "5517", + "routeType": "지선", + "startStation": "한남운수대학동차고지", + "endStation": "중앙대학교" + }, + { + "routeID": 100100255, + "busRouteName": "5519", + "routeType": "지선", + "startStation": "우방아파트", + "endStation": "용천사" + }, + { + "routeID": 100100259, + "busRouteName": "5523", + "routeType": "지선", + "startStation": "서울대입구역", + "endStation": "난곡종점" + }, + { + "routeID": 100100260, + "busRouteName": "5524", + "routeType": "지선", + "startStation": "난향차고지", + "endStation": "중앙대학교" + }, + { + "routeID": 100100261, + "busRouteName": "5525", + "routeType": "지선", + "startStation": "시흥동", + "endStation": "보라매공원" + }, + { + "routeID": 100100263, + "busRouteName": "5528", + "routeType": "지선", + "startStation": "가산동", + "endStation": "사당역" + }, + { + "routeID": 100100264, + "busRouteName": "5530", + "routeType": "지선", + "startStation": "군포공영차고지", + "endStation": "사당역" + }, + { + "routeID": 100100265, + "busRouteName": "5531", + "routeType": "지선", + "startStation": "군포공영차고지", + "endStation": "노들역" + }, + { + "routeID": 100100266, + "busRouteName": "5634", + "routeType": "지선", + "startStation": "광명공영차고지", + "endStation": "여의도" + }, + { + "routeID": 100100267, + "busRouteName": "5535", + "routeType": "지선", + "startStation": "하안동", + "endStation": "노량진" + }, + { + "routeID": 100100268, + "busRouteName": "5536", + "routeType": "지선", + "startStation": "하안동", + "endStation": "노량진" + }, + { + "routeID": 100100269, + "busRouteName": "5537", + "routeType": "지선", + "startStation": "시흥동", + "endStation": "가산디지털단지역" + }, + { + "routeID": 100100272, + "busRouteName": "5615", + "routeType": "지선", + "startStation": "여의도", + "endStation": "난곡" + }, + { + "routeID": 100100273, + "busRouteName": "5616", + "routeType": "지선", + "startStation": "가산동기점", + "endStation": "영동중학교" + }, + { + "routeID": 100100274, + "busRouteName": "5617", + "routeType": "지선", + "startStation": "시흥", + "endStation": "구로디지털단지역" + }, + { + "routeID": 100100275, + "busRouteName": "5618", + "routeType": "지선", + "startStation": "구로동", + "endStation": "구로동" + }, + { + "routeID": 100100276, + "busRouteName": "5619", + "routeType": "지선", + "startStation": "시흥동", + "endStation": "신도림역" + }, + { + "routeID": 100100277, + "busRouteName": "5620", + "routeType": "지선", + "startStation": "시흥", + "endStation": "선유도역" + }, + { + "routeID": 100100278, + "busRouteName": "5621", + "routeType": "지선", + "startStation": "삼익아파트", + "endStation": "구로디지털단지역" + }, + { + "routeID": 100100279, + "busRouteName": "5623", + "routeType": "지선", + "startStation": "군포공영차고지", + "endStation": "여의도" + }, + { + "routeID": 100100280, + "busRouteName": "5624", + "routeType": "지선", + "startStation": "부곡버스공영차고지", + "endStation": "구로디지털단지역" + }, + { + "routeID": 100100281, + "busRouteName": "5625", + "routeType": "지선", + "startStation": "안양비산동", + "endStation": "영등포역(영등포시장)" + }, + { + "routeID": 100100282, + "busRouteName": "5626", + "routeType": "지선", + "startStation": "안양비산동", + "endStation": "온수동종점" + }, + { + "routeID": 100100283, + "busRouteName": "5627", + "routeType": "지선", + "startStation": "노온사동", + "endStation": "구로디지털단지역" + }, + { + "routeID": 100100284, + "busRouteName": "5630", + "routeType": "지선", + "startStation": "광명공영차고지", + "endStation": "목동역" + }, + { + "routeID": 100100285, + "busRouteName": "5633", + "routeType": "지선", + "startStation": "노온사동", + "endStation": "순복음교회" + }, + { + "routeID": 100100286, + "busRouteName": "5712", + "routeType": "지선", + "startStation": "가산동기점", + "endStation": "홍대입구역" + }, + { + "routeID": 100100287, + "busRouteName": "5713", + "routeType": "지선", + "startStation": "안양비산동", + "endStation": "신촌기차역" + }, + { + "routeID": 100100288, + "busRouteName": "5714", + "routeType": "지선", + "startStation": "광명공영차고지", + "endStation": "이대입구" + }, + { + "routeID": 100100289, + "busRouteName": "6211", + "routeType": "지선", + "startStation": "신월동", + "endStation": "상왕십리" + }, + { + "routeID": 100100290, + "busRouteName": "6411", + "routeType": "지선", + "startStation": "구로동", + "endStation": "개포동" + }, + { + "routeID": 100100291, + "busRouteName": "6511", + "routeType": "지선", + "startStation": "구로동", + "endStation": "서울대" + }, + { + "routeID": 100100292, + "busRouteName": "6512", + "routeType": "지선", + "startStation": "구로동", + "endStation": "서울대" + }, + { + "routeID": 100100293, + "busRouteName": "6513", + "routeType": "지선", + "startStation": "철산동", + "endStation": "서울대" + }, + { + "routeID": 100100294, + "busRouteName": "6514", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "서울대학교" + }, + { + "routeID": 100100295, + "busRouteName": "6611", + "routeType": "지선", + "startStation": "신도림역", + "endStation": "우성아파트" + }, + { + "routeID": 100100297, + "busRouteName": "6614", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "부천옥길지구" + }, + { + "routeID": 100100298, + "busRouteName": "6616", + "routeType": "지선", + "startStation": "철산동", + "endStation": "온수동" + }, + { + "routeID": 100100299, + "busRouteName": "6617", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "목동우성아파트" + }, + { + "routeID": 100100300, + "busRouteName": "6620", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "당산역" + }, + { + "routeID": 100100301, + "busRouteName": "6623", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "여의도" + }, + { + "routeID": 100100302, + "busRouteName": "6624", + "routeType": "지선", + "startStation": "신월동", + "endStation": "이대목동병원" + }, + { + "routeID": 100100303, + "busRouteName": "6625", + "routeType": "지선", + "startStation": "문래동", + "endStation": "화곡역" + }, + { + "routeID": 100100304, + "busRouteName": "6627", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "이대목동병원" + }, + { + "routeID": 100100305, + "busRouteName": "6628", + "routeType": "지선", + "startStation": "외발산동", + "endStation": "여의도" + }, + { + "routeID": 100100306, + "busRouteName": "6629", + "routeType": "지선", + "startStation": "방화동", + "endStation": "영등포역" + }, + { + "routeID": 100100307, + "busRouteName": "6630", + "routeType": "지선", + "startStation": "영인운수차고지", + "endStation": "영등포시장" + }, + { + "routeID": 100100308, + "busRouteName": "6631", + "routeType": "지선", + "startStation": "강서공영차고지.개화역", + "endStation": "영등포시장" + }, + { + "routeID": 100100309, + "busRouteName": "6632", + "routeType": "지선", + "startStation": "강서공영차고지.개화역", + "endStation": "당산역" + }, + { + "routeID": 100100311, + "busRouteName": "6635", + "routeType": "지선", + "startStation": "광명 하안동", + "endStation": "구로공단역" + }, + { + "routeID": 100100312, + "busRouteName": "6637", + "routeType": "지선", + "startStation": "노온사동", + "endStation": "목동" + }, + { + "routeID": 100100313, + "busRouteName": "6638", + "routeType": "지선", + "startStation": "철산동", + "endStation": "오목교" + }, + { + "routeID": 100100316, + "busRouteName": "6642", + "routeType": "지선", + "startStation": "강서공영차고지.개화역", + "endStation": "가양3동도시개발9단지아파트" + }, + { + "routeID": 100100318, + "busRouteName": "6645", + "routeType": "지선", + "startStation": "강서공영차고지.개화역", + "endStation": "강서공영차고지.개화역" + }, + { + "routeID": 100100320, + "busRouteName": "6647", + "routeType": "지선", + "startStation": "강서공영차고지.개화역", + "endStation": "강서공영차고지.개화역" + }, + { + "routeID": 100100322, + "busRouteName": "6654", + "routeType": "지선", + "startStation": "신풍역", + "endStation": "여의도역" + }, + { + "routeID": 100100329, + "busRouteName": "6657", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "강서한강자이아파트" + }, + { + "routeID": 100100330, + "busRouteName": "6712", + "routeType": "지선", + "startStation": "방화동", + "endStation": "서강대학교" + }, + { + "routeID": 100100331, + "busRouteName": "6714", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "이대부고" + }, + { + "routeID": 100100332, + "busRouteName": "6715", + "routeType": "지선", + "startStation": "신월동", + "endStation": "상암동" + }, + { + "routeID": 100100337, + "busRouteName": "7017", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "롯데백화점" + }, + { + "routeID": 100100338, + "busRouteName": "7018", + "routeType": "지선", + "startStation": "북가좌동", + "endStation": "무교동" + }, + { + "routeID": 100100339, + "busRouteName": "7019", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "서소문" + }, + { + "routeID": 100100340, + "busRouteName": "7021", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "을지로입구" + }, + { + "routeID": 100100341, + "busRouteName": "7022", + "routeType": "지선", + "startStation": "구산동", + "endStation": "서울역" + }, + { + "routeID": 100100342, + "busRouteName": "7024", + "routeType": "지선", + "startStation": "봉원사", + "endStation": "서울역" + }, + { + "routeID": 100100344, + "busRouteName": "7211", + "routeType": "지선", + "startStation": "진관공영차고지", + "endStation": "신설동" + }, + { + "routeID": 100100345, + "busRouteName": "7611", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "여의도" + }, + { + "routeID": 100100346, + "busRouteName": "7612", + "routeType": "지선", + "startStation": "홍연2교", + "endStation": "영등포구청역" + }, + { + "routeID": 100100347, + "busRouteName": "7613", + "routeType": "지선", + "startStation": "구산동", + "endStation": "여의도" + }, + { + "routeID": 100100348, + "busRouteName": "7711", + "routeType": "지선", + "startStation": "덕은동종점", + "endStation": "홍대입구역" + }, + { + "routeID": 100100349, + "busRouteName": "7713", + "routeType": "지선", + "startStation": "홍연2교", + "endStation": "홍연2교" + }, + { + "routeID": 100100352, + "busRouteName": "7720", + "routeType": "지선", + "startStation": "구산동", + "endStation": "신촌" + }, + { + "routeID": 100100353, + "busRouteName": "7722", + "routeType": "지선", + "startStation": "진관공영차고지", + "endStation": "녹번역" + }, + { + "routeID": 100100354, + "busRouteName": "7723", + "routeType": "지선", + "startStation": "진관공영차고지", + "endStation": "구파발역" + }, + { + "routeID": 100100357, + "busRouteName": "7726", + "routeType": "지선", + "startStation": "덕은동종점", + "endStation": "모래내삼거리" + }, + { + "routeID": 100100358, + "busRouteName": "7727", + "routeType": "지선", + "startStation": "설문동", + "endStation": "신촌" + }, + { + "routeID": 100100359, + "busRouteName": "7728", + "routeType": "지선", + "startStation": "대화동", + "endStation": "신촌" + }, + { + "routeID": 100100360, + "busRouteName": "7730", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "이북오도청" + }, + { + "routeID": 100100363, + "busRouteName": "7737", + "routeType": "지선", + "startStation": "은평공영차고지", + "endStation": "파크빌아파트" + }, + { + "routeID": 100100364, + "busRouteName": "7738", + "routeType": "지선", + "startStation": "은평공영차고지", + "endStation": "홍제역" + }, + { + "routeID": 100100383, + "busRouteName": "8541", + "routeType": "맞춤", + "startStation": "호압사", + "endStation": "강남역" + }, + { + "routeID": 100100387, + "busRouteName": "8774", + "routeType": "맞춤", + "startStation": "구산동", + "endStation": "서대문구청" + }, + { + "routeID": 100100389, + "busRouteName": "9401", + "routeType": "광역", + "startStation": "구미동차고지", + "endStation": "서울역" + }, + { + "routeID": 100100390, + "busRouteName": "9403", + "routeType": "광역", + "startStation": "구미동차고지", + "endStation": "동대문역사문화공원" + }, + { + "routeID": 100100391, + "busRouteName": "9404", + "routeType": "광역", + "startStation": "분당구미", + "endStation": "신사역" + }, + { + "routeID": 100100392, + "busRouteName": "9408", + "routeType": "광역", + "startStation": "분당구 구미동", + "endStation": "영등포" + }, + { + "routeID": 100100397, + "busRouteName": "9701", + "routeType": "광역", + "startStation": "고양시 가좌동", + "endStation": "롯데영프라자" + }, + { + "routeID": 100100398, + "busRouteName": "9703", + "routeType": "광역", + "startStation": "신성교통차고지", + "endStation": "서울역" + }, + { + "routeID": 100100400, + "busRouteName": "9707", + "routeType": "광역", + "startStation": "고양 가좌동", + "endStation": "영등포역" + }, + { + "routeID": 100100406, + "busRouteName": "9714", + "routeType": "광역", + "startStation": "교하 운정", + "endStation": "서울역" + }, + { + "routeID": 100100407, + "busRouteName": "240", + "routeType": "간선", + "startStation": "중랑공영차고지", + "endStation": "신사역사거리" + }, + { + "routeID": 100100409, + "busRouteName": "421", + "routeType": "간선", + "startStation": "염곡동차고지", + "endStation": "옥수동" + }, + { + "routeID": 100100410, + "busRouteName": "502", + "routeType": "간선", + "startStation": "월암공영차고지", + "endStation": "한국은행" + }, + { + "routeID": 100100425, + "busRouteName": "1122", + "routeType": "지선", + "startStation": "석관동(상진운수종점)", + "endStation": "원자력병원" + }, + { + "routeID": 100100427, + "busRouteName": "4319", + "routeType": "지선", + "startStation": "전원마을", + "endStation": "잠실역" + }, + { + "routeID": 100100437, + "busRouteName": "771", + "routeType": "간선", + "startStation": "대화동종점", + "endStation": "디지털미디어시티역" + }, + { + "routeID": 100100438, + "busRouteName": "3011", + "routeType": "지선", + "startStation": "장지공영차고지", + "endStation": "한남동" + }, + { + "routeID": 100100440, + "busRouteName": "700", + "routeType": "간선", + "startStation": "대화동", + "endStation": "서울역" + }, + { + "routeID": 100100446, + "busRouteName": "7025", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "종로6가" + }, + { + "routeID": 100100447, + "busRouteName": "7016", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "상명대" + }, + { + "routeID": 100100448, + "busRouteName": "7013B", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "남대문시장" + }, + { + "routeID": 100100449, + "busRouteName": "7013A", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "남대문시장" + }, + { + "routeID": 100100450, + "busRouteName": "7011", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "중구청" + }, + { + "routeID": 100100451, + "busRouteName": "6716", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "이대입구" + }, + { + "routeID": 100100453, + "busRouteName": "6613", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "대림역" + }, + { + "routeID": 100100454, + "busRouteName": "4419", + "routeType": "지선", + "startStation": "송파차고지", + "endStation": "압구정역3번출구" + }, + { + "routeID": 100100459, + "busRouteName": "440", + "routeType": "간선", + "startStation": "송파공영차고지", + "endStation": "압구정동한양파출소앞" + }, + { + "routeID": 100100462, + "busRouteName": "7715", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "연신내역" + }, + { + "routeID": 100100478, + "busRouteName": "3317", + "routeType": "지선", + "startStation": "남한산성입구", + "endStation": "잠실리센츠아파트앞" + }, + { + "routeID": 100100495, + "busRouteName": "672", + "routeType": "간선", + "startStation": "방화동기점", + "endStation": "이대후문" + }, + { + "routeID": 100100496, + "busRouteName": "333", + "routeType": "간선", + "startStation": "송파공영차고지", + "endStation": "올림픽공원" + }, + { + "routeID": 100100497, + "busRouteName": "653", + "routeType": "간선", + "startStation": "영인운수차고지", + "endStation": "가산디지털단지역" + }, + { + "routeID": 100100498, + "busRouteName": "3318", + "routeType": "지선", + "startStation": "강동공영차고지", + "endStation": "마천동" + }, + { + "routeID": 100100499, + "busRouteName": "7212", + "routeType": "지선", + "startStation": "은평차고지", + "endStation": "극동그린아파트앞" + }, + { + "routeID": 100100500, + "busRouteName": "4312", + "routeType": "지선", + "startStation": "개포동", + "endStation": "가락1동주민센터" + }, + { + "routeID": 100100501, + "busRouteName": "3319", + "routeType": "지선", + "startStation": "장지공영차고지", + "endStation": "잠실역7번출구" + }, + { + "routeID": 100100511, + "busRouteName": "707", + "routeType": "간선", + "startStation": "대화동", + "endStation": "서울역" + }, + { + "routeID": 100100521, + "busRouteName": "6515", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "경인교육대학교" + }, + { + "routeID": 100100522, + "busRouteName": "2016", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "효창공원후문" + }, + { + "routeID": 100100525, + "busRouteName": "8772", + "routeType": "맞춤", + "startStation": "진관공영차고지", + "endStation": "북한산성입구" + }, + { + "routeID": 100100537, + "busRouteName": "740", + "routeType": "간선", + "startStation": "덕은동종점", + "endStation": "무역센터삼성역" + }, + { + "routeID": 100100549, + "busRouteName": "100", + "routeType": "간선", + "startStation": "하계동", + "endStation": "용산구청" + }, + { + "routeID": 100100550, + "busRouteName": "662", + "routeType": "간선", + "startStation": "외발산동", + "endStation": "여의나루역" + }, + { + "routeID": 100100551, + "busRouteName": "673", + "routeType": "간선", + "startStation": "부천상동", + "endStation": "이대부고" + }, + { + "routeID": 100100552, + "busRouteName": "7739", + "routeType": "지선", + "startStation": "은평공영차고지", + "endStation": "서교가든" + }, + { + "routeID": 100100553, + "busRouteName": "350", + "routeType": "간선", + "startStation": "송파차고지", + "endStation": "노들역앞" + }, + { + "routeID": 100100564, + "busRouteName": "320", + "routeType": "간선", + "startStation": "송파차고지", + "endStation": "중랑구청" + }, + { + "routeID": 100100565, + "busRouteName": "750B", + "routeType": "간선", + "startStation": "은평차고지", + "endStation": "서울대학교" + }, + { + "routeID": 100100566, + "busRouteName": "1115", + "routeType": "지선", + "startStation": "수유중학교혜화여고", + "endStation": "미아삼거리역" + }, + { + "routeID": 100100574, + "busRouteName": "541", + "routeType": "간선", + "startStation": "군포공영차고지", + "endStation": "강남역" + }, + { + "routeID": 100100576, + "busRouteName": "6516", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "박미삼거리" + }, + { + "routeID": 100100578, + "busRouteName": "3321", + "routeType": "지선", + "startStation": "강동공영차고지", + "endStation": "강동구청" + }, + { + "routeID": 100100579, + "busRouteName": "3012", + "routeType": "지선", + "startStation": "송파차고지", + "endStation": "이촌2동" + }, + { + "routeID": 100100583, + "busRouteName": "121", + "routeType": "간선", + "startStation": "화계사", + "endStation": "서울숲" + }, + { + "routeID": 100100587, + "busRouteName": "705", + "routeType": "간선", + "startStation": "은평뉴타운공영차고지", + "endStation": "롯데백화점" + }, + { + "routeID": 100100591, + "busRouteName": "N30", + "routeType": "심야", + "startStation": "강동공영차고지", + "endStation": "서울역환승센터" + }, + { + "routeID": 100100595, + "busRouteName": "241", + "routeType": "간선", + "startStation": "중랑공영차고지", + "endStation": "논현역" + }, + { + "routeID": 100100596, + "busRouteName": "400", + "routeType": "간선", + "startStation": "염곡동", + "endStation": "시청앞" + }, + { + "routeID": 100100597, + "busRouteName": "405", + "routeType": "간선", + "startStation": "염곡동", + "endStation": "시청광장" + }, + { + "routeID": 100100598, + "busRouteName": "2115", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "서경대입구" + }, + { + "routeID": 100100599, + "busRouteName": "2311", + "routeType": "지선", + "startStation": "중랑공영차고지", + "endStation": "문정동" + }, + { + "routeID": 100100601, + "busRouteName": "6640A", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "양천공영차고지" + }, + { + "routeID": 100100602, + "busRouteName": "6640B", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "양천공영차고지" + }, + { + "routeID": 100100603, + "busRouteName": "542", + "routeType": "간선", + "startStation": "군포복합화물터미널", + "endStation": "신사역" + }, + { + "routeID": 100100604, + "busRouteName": "4211", + "routeType": "지선", + "startStation": "염곡동차고지", + "endStation": "한양대동문앞" + }, + { + "routeID": 100100605, + "busRouteName": "463", + "routeType": "간선", + "startStation": "염곡동차고지", + "endStation": "국회의사당" + }, + { + "routeID": 100100607, + "busRouteName": "9711", + "routeType": "광역", + "startStation": "신성교통차고지", + "endStation": "양재동" + }, + { + "routeID": 100100609, + "busRouteName": "3425", + "routeType": "지선", + "startStation": "송파공영차고지", + "endStation": "삼성역" + }, + { + "routeID": 100100612, + "busRouteName": "3426", + "routeType": "지선", + "startStation": "서울버스종점", + "endStation": "청담동" + }, + { + "routeID": 100100613, + "busRouteName": "3322", + "routeType": "지선", + "startStation": "송파차고지", + "endStation": "잠실종합운동장" + }, + { + "routeID": 103000001, + "busRouteName": "8441", + "routeType": "맞춤", + "startStation": "은곡마을", + "endStation": "LH이편한세상" + }, + { + "routeID": 104000006, + "busRouteName": "242", + "routeType": "간선", + "startStation": "중랑공영차고지", + "endStation": "개포동" + }, + { + "routeID": 104000007, + "busRouteName": "01A", + "routeType": "순환", + "startStation": "서울역버스환승센터", + "endStation": "서울역버스환승센터" + }, + { + "routeID": 104000008, + "busRouteName": "01B", + "routeType": "순환", + "startStation": "서울역버스환승센터", + "endStation": "서울역버스환승센터" + }, + { + "routeID": 106000001, + "busRouteName": "8221", + "routeType": "맞춤", + "startStation": "장안2동주민센터", + "endStation": "답십리역" + }, + { + "routeID": 107000001, + "busRouteName": "1116", + "routeType": "지선", + "startStation": "국민대학교", + "endStation": "미아사거리" + }, + { + "routeID": 107000002, + "busRouteName": "343", + "routeType": "간선", + "startStation": "송파공영차고지", + "endStation": "수서역" + }, + { + "routeID": 107000003, + "busRouteName": "8002", + "routeType": "지선", + "startStation": "상명대앞", + "endStation": "경복궁역" + }, + { + "routeID": 107000004, + "busRouteName": "8003", + "routeType": "지선", + "startStation": "평창동주민센터", + "endStation": "평창동주민센터" + }, + { + "routeID": 108000001, + "busRouteName": "8551", + "routeType": "맞춤", + "startStation": "봉천역", + "endStation": "노량진역" + }, + { + "routeID": 108000002, + "busRouteName": "1167", + "routeType": "지선", + "startStation": "우이동", + "endStation": "은행사거리(11414)" + }, + { + "routeID": 110000002, + "busRouteName": "173", + "routeType": "간선", + "startStation": "월계동", + "endStation": "신촌기차역" + }, + { + "routeID": 110000003, + "busRouteName": "8112", + "routeType": "맞춤", + "startStation": "장위뉴타운", + "endStation": "성신여대입구" + }, + { + "routeID": 111000008, + "busRouteName": "674", + "routeType": "간선", + "startStation": "신길운수", + "endStation": "연세대" + }, + { + "routeID": 111000009, + "busRouteName": "8771", + "routeType": "맞춤", + "startStation": "구산중", + "endStation": "구산교회" + }, + { + "routeID": 111000010, + "busRouteName": "7734", + "routeType": "지선", + "startStation": "진관공영차고지", + "endStation": "홍대입구역" + }, + { + "routeID": 111000011, + "busRouteName": "773", + "routeType": "간선", + "startStation": "교하운정", + "endStation": "불광역" + }, + { + "routeID": 111000012, + "busRouteName": "774", + "routeType": "간선", + "startStation": "진관공영차고지", + "endStation": "파주읍" + }, + { + "routeID": 111000014, + "busRouteName": "761", + "routeType": "간선", + "startStation": "진관공영차고지", + "endStation": "영등포역" + }, + { + "routeID": 112000001, + "busRouteName": "8777", + "routeType": "맞춤", + "startStation": "난지캠핑장", + "endStation": "월드컵경기장남측" + }, + { + "routeID": 113000001, + "busRouteName": "8761", + "routeType": "맞춤", + "startStation": "신촌로터리", + "endStation": "국회의사당" + }, + { + "routeID": 113000002, + "busRouteName": "452", + "routeType": "간선", + "startStation": "송파공영차고지", + "endStation": "중앙대학교" + }, + { + "routeID": 114000001, + "busRouteName": "6649", + "routeType": "지선", + "startStation": "신도림역", + "endStation": "오목교역청학스포츠타운" + }, + { + "routeID": 115000005, + "busRouteName": "6648", + "routeType": "지선", + "startStation": "방화동", + "endStation": "양천구청" + }, + { + "routeID": 115000007, + "busRouteName": "654", + "routeType": "간선", + "startStation": "방화동", + "endStation": "노들역" + }, + { + "routeID": 116000001, + "busRouteName": "8552", + "routeType": "맞춤", + "startStation": "신림복지관앞", + "endStation": "신림역신림사거리" + }, + { + "routeID": 116000002, + "busRouteName": "6615", + "routeType": "지선", + "startStation": "양천공영차고지", + "endStation": "천왕역" + }, + { + "routeID": 116000004, + "busRouteName": "660", + "routeType": "간선", + "startStation": "온수동종점", + "endStation": "가양역" + }, + { + "routeID": 122000001, + "busRouteName": "4435", + "routeType": "지선", + "startStation": "개포동차고지", + "endStation": "사당역" + }, + { + "routeID": 123000010, + "busRouteName": "741", + "routeType": "간선", + "startStation": "진관차고지", + "endStation": "세곡동사거리" + }, + { + "routeID": 123000011, + "busRouteName": "708", + "routeType": "간선", + "startStation": "진관차고지", + "endStation": "서울역" + }, + { + "routeID": 124000008, + "busRouteName": "7719", + "routeType": "지선", + "startStation": "북가좌동", + "endStation": "녹번동" + }, + { + "routeID": 124000010, + "busRouteName": "8331", + "routeType": "맞춤", + "startStation": "마천사거리", + "endStation": "잠실역" + }, + { + "routeID": 124000036, + "busRouteName": "362", + "routeType": "간선", + "startStation": "송파공영차고지", + "endStation": "여의도" + }, + { + "routeID": 124000038, + "busRouteName": "342", + "routeType": "간선", + "startStation": "강동차고지", + "endStation": "압구정로데오역" + }, + { + "routeID": 100100124, + "busRouteName": "0017", + "routeType": "지선", + "startStation": "청암자이아파트", + "endStation": "청암동강변삼성아파트" + }, + { + "routeID": 100100001, + "busRouteName": "02", + "routeType": "순환", + "startStation": "남산예장버스환승주차장", + "endStation": "남산예장버스환승주차장" + }, + { + "routeID": 106000002, + "busRouteName": "04", + "routeType": "순환", + "startStation": "남산예장버스환승주차장", + "endStation": "남산예장버스환승주차장" + }, + { + "routeID": 100100016, + "busRouteName": "110A고려대", + "routeType": "간선", + "startStation": "정릉북한산국립공원입구", + "endStation": "정릉북한산국립공원입구" + }, + { + "routeID": 100100015, + "busRouteName": "110B국민대", + "routeType": "간선", + "startStation": "정릉북한산국립공원입구", + "endStation": "정릉북한산국립공원입구" + }, + { + "routeID": 100100611, + "busRouteName": "2312", + "routeType": "지선", + "startStation": "강동공영차고지", + "endStation": "중랑공영차고지·신내역" + }, + { + "routeID": 100100237, + "busRouteName": "4318", + "routeType": "지선", + "startStation": "남태령역", + "endStation": "풍납1동동아한가람아파트" + }, + { + "routeID": 100100257, + "busRouteName": "5522A난곡", + "routeType": "지선", + "startStation": "난향공영차고지", + "endStation": "난향공영차고지" + }, + { + "routeID": 100100258, + "busRouteName": "5522B호암", + "routeType": "지선", + "startStation": "난향공영차고지", + "endStation": "난향공영차고지" + }, + { + "routeID": 100100507, + "busRouteName": "6009", + "routeType": "공항", + "startStation": "가락1동주민센터", + "endStation": "인천국제공항 제2터미널" + }, + { + "routeID": 100100513, + "busRouteName": "6100", + "routeType": "공항", + "startStation": "망우역경의중앙선", + "endStation": "인천국제공항 제2터미널" + }, + { + "routeID": 100100373, + "busRouteName": "6101", + "routeType": "공항", + "startStation": "창동역", + "endStation": "김포공항국내선" + }, + { + "routeID": 100100104, + "busRouteName": "702A서오릉", + "routeType": "간선", + "startStation": "용두동종점", + "endStation": "을지로입구역.광교" + }, + { + "routeID": 100100105, + "busRouteName": "702B용두초교", + "routeType": "간선", + "startStation": "용두사거리", + "endStation": "을지로입구역.광교" + }, + { + "routeID": 100100593, + "busRouteName": "N13", + "routeType": "심야", + "startStation": "온곡중학교노원정보도서관", + "endStation": "복정역환승센터" + }, + { + "routeID": 100100610, + "busRouteName": "N15", + "routeType": "심야", + "startStation": "우이동성원아파트", + "endStation": "남태령역" + }, + { + "routeID": 100100592, + "busRouteName": "N16", + "routeType": "심야", + "startStation": "도봉산역광역환승센터", + "endStation": "온수동종점" + }, + { + "routeID": 100100586, + "busRouteName": "N26", + "routeType": "심야", + "startStation": "중랑공영차고지·신내역", + "endStation": "개화역광역환승센터" + }, + { + "routeID": 100100585, + "busRouteName": "N37", + "routeType": "심야", + "startStation": "복정역환승센터", + "endStation": "진관공영차고지" + }, + { + "routeID": 100100589, + "busRouteName": "N61", + "routeType": "심야", + "startStation": "양천공영차고지", + "endStation": "상계주공7단지" + }, + { + "routeID": 100100588, + "busRouteName": "N62", + "routeType": "심야", + "startStation": "양천공영차고지", + "endStation": "중화중학교" + }, + { + "routeID": 115000008, + "busRouteName": "N65", + "routeType": "심야", + "startStation": "개화역광역환승센터", + "endStation": "범일운수종점" + }, + { + "routeID": 100900016, + "busRouteName": "TOUR01", + "routeType": "순환", + "startStation": "광화문", + "endStation": "광화문" + }, + { + "routeID": 101000001, + "busRouteName": "TOUR02", + "routeType": "순환", + "startStation": "광화문", + "endStation": "광화문" + }, + { + "routeID": 101000002, + "busRouteName": "TOUR03", + "routeType": "순환", + "startStation": "강남역", + "endStation": "강남역" + }, + { + "routeID": 101000003, + "busRouteName": "TOUR04", + "routeType": "순환", + "startStation": "광화문", + "endStation": "광화문" + }, + { + "routeID": 100000017, + "busRouteName": "TOUR11", + "routeType": "순환", + "startStation": "동대문디자인플라자", + "endStation": "동대문디자인플라자" + }, + { + "routeID": 100000018, + "busRouteName": "TOUR12", + "routeType": "순환", + "startStation": "동대문디자인플라자", + "endStation": "동대문디자인플라자" + }, + { + "routeID": 122900003, + "busRouteName": "강남01", + "routeType": "마을", + "startStation": "일원한솔아파트", + "endStation": "노블발렌티웨딩홀" + }, + { + "routeID": 122900006, + "busRouteName": "강남02", + "routeType": "마을", + "startStation": "강남구민체육관", + "endStation": "양재역" + }, + { + "routeID": 122900004, + "busRouteName": "강남03", + "routeType": "마을", + "startStation": "강남데시앙포레", + "endStation": "세곡리엔파크" + }, + { + "routeID": 122900002, + "busRouteName": "강남05", + "routeType": "마을", + "startStation": "구룡마을", + "endStation": "석촌한솔아파트" + }, + { + "routeID": 122900005, + "busRouteName": "강남06", + "routeType": "마을", + "startStation": "세곡푸르지오", + "endStation": "무역센터" + }, + { + "routeID": 122900007, + "busRouteName": "강남06-1", + "routeType": "마을", + "startStation": "리엔파크4단지·강남데시앙파크", + "endStation": "한아름아파트.사이룩스오피스텔" + }, + { + "routeID": 122900010, + "busRouteName": "강남06-2", + "routeType": "마을", + "startStation": "리엔파크4단지·강남데시앙파크", + "endStation": "한아름아파트.사이룩스오피스텔" + }, + { + "routeID": 122900001, + "busRouteName": "강남07", + "routeType": "마을", + "startStation": "서울의료원후문", + "endStation": "양재역.말죽거리" + }, + { + "routeID": 122900008, + "busRouteName": "강남08", + "routeType": "마을", + "startStation": "신사사거리", + "endStation": "한국무역센터·삼성역" + }, + { + "routeID": 122900009, + "busRouteName": "강남10", + "routeType": "마을", + "startStation": "개포주공4·7단지", + "endStation": "양재역" + }, + { + "routeID": 124900002, + "busRouteName": "강동01", + "routeType": "마을", + "startStation": "신동아아파트", + "endStation": "테크노마트앞·강변역" + }, + { + "routeID": 124900003, + "busRouteName": "강동02", + "routeType": "마을", + "startStation": "가래여울", + "endStation": "서원마을" + }, + { + "routeID": 124900001, + "busRouteName": "강동05", + "routeType": "마을", + "startStation": "강동공영차고지", + "endStation": "천호역" + }, + { + "routeID": 108900006, + "busRouteName": "강북01", + "routeType": "마을", + "startStation": "아카데미하우스·통일교육원", + "endStation": "수유역.강북구청" + }, + { + "routeID": 108900002, + "busRouteName": "강북02", + "routeType": "마을", + "startStation": "본원정사", + "endStation": "수유역.강북구청" + }, + { + "routeID": 108900003, + "busRouteName": "강북03", + "routeType": "마을", + "startStation": "빨래골종점", + "endStation": "수유역.강북구청" + }, + { + "routeID": 108900005, + "busRouteName": "강북04", + "routeType": "마을", + "startStation": "연일슈퍼", + "endStation": "수유시장" + }, + { + "routeID": 108900008, + "busRouteName": "강북05", + "routeType": "마을", + "startStation": "번동초등학교", + "endStation": "삼우하이츠빌라" + }, + { + "routeID": 108900007, + "busRouteName": "강북06", + "routeType": "마을", + "startStation": "번2동주민센터", + "endStation": "미아사거리역" + }, + { + "routeID": 108900013, + "busRouteName": "강북08", + "routeType": "마을", + "startStation": "두산위브트레지움", + "endStation": "현대백화점별관주차장" + }, + { + "routeID": 108900004, + "busRouteName": "강북09", + "routeType": "마을", + "startStation": "현대백화점·E마트·빅토리아호텔", + "endStation": "수유역·강북구청" + }, + { + "routeID": 108900009, + "busRouteName": "강북10", + "routeType": "마을", + "startStation": "솔샘터널", + "endStation": "미아역" + }, + { + "routeID": 108900001, + "busRouteName": "강북11", + "routeType": "마을", + "startStation": "현대백화점·E마트·빅토리아호텔", + "endStation": "수유역.강북구청" + }, + { + "routeID": 108900012, + "busRouteName": "강북12", + "routeType": "마을", + "startStation": "삼각산119안전센터", + "endStation": "현대백화점별관주차장" + }, + { + "routeID": 115900006, + "busRouteName": "강서01", + "routeType": "마을", + "startStation": "약수터", + "endStation": "등촌역" + }, + { + "routeID": 115900003, + "busRouteName": "강서02", + "routeType": "마을", + "startStation": "남부시장", + "endStation": "하이웨이주유소" + }, + { + "routeID": 115900004, + "busRouteName": "강서03", + "routeType": "마을", + "startStation": "약수터", + "endStation": "약수터" + }, + { + "routeID": 115900001, + "busRouteName": "강서04", + "routeType": "마을", + "startStation": "염창역·강서평생학습관", + "endStation": "강서구청사거리" + }, + { + "routeID": 115900005, + "busRouteName": "강서05", + "routeType": "마을", + "startStation": "발산역.NC백화점", + "endStation": "강서농수산물도매시장" + }, + { + "routeID": 115900008, + "busRouteName": "강서05-1", + "routeType": "마을", + "startStation": "다솔마을", + "endStation": "마곡나루역" + }, + { + "routeID": 115900002, + "busRouteName": "강서06", + "routeType": "마을", + "startStation": "벽산아파트", + "endStation": "수명산입구" + }, + { + "routeID": 115900007, + "busRouteName": "강서07", + "routeType": "마을", + "startStation": "생태공원", + "endStation": "마곡역" + }, + { + "routeID": 120900005, + "busRouteName": "관악01", + "routeType": "마을", + "startStation": "봉천역", + "endStation": "숭실대입구역" + }, + { + "routeID": 120900008, + "busRouteName": "관악02", + "routeType": "마을", + "startStation": "낙성대역", + "endStation": "서울대학교제2공학관" + }, + { + "routeID": 120900003, + "busRouteName": "관악03", + "routeType": "마을", + "startStation": "국사봉", + "endStation": "신림역" + }, + { + "routeID": 120900009, + "busRouteName": "관악04", + "routeType": "마을", + "startStation": "군인아파트", + "endStation": "사랑의병원" + }, + { + "routeID": 120900010, + "busRouteName": "관악05", + "routeType": "마을", + "startStation": "신림현대아파트", + "endStation": "봉림교" + }, + { + "routeID": 120900004, + "busRouteName": "관악06", + "routeType": "마을", + "startStation": "보명사·법화사", + "endStation": "신대방역" + }, + { + "routeID": 120900006, + "busRouteName": "관악07", + "routeType": "마을", + "startStation": "우성아파트", + "endStation": "서울대입구역" + }, + { + "routeID": 120900007, + "busRouteName": "관악08", + "routeType": "마을", + "startStation": "난향공영차고지", + "endStation": "신림역" + }, + { + "routeID": 120900002, + "busRouteName": "관악10", + "routeType": "마을", + "startStation": "아카시아마을·민방위교육장", + "endStation": "신림역" + }, + { + "routeID": 114900004, + "busRouteName": "관악11", + "routeType": "마을", + "startStation": "봉천역", + "endStation": "재넘이고개" + }, + { + "routeID": 104900005, + "busRouteName": "광진01", + "routeType": "마을", + "startStation": "광진정보도서관", + "endStation": "워커힐아파트" + }, + { + "routeID": 104900003, + "busRouteName": "광진02", + "routeType": "마을", + "startStation": "긴고랑종점", + "endStation": "군자역" + }, + { + "routeID": 104900002, + "busRouteName": "광진03", + "routeType": "마을", + "startStation": "용암사입구", + "endStation": "테크노마트앞·강변역" + }, + { + "routeID": 104900001, + "busRouteName": "광진04", + "routeType": "마을", + "startStation": "중곡아파트", + "endStation": "테크노마트앞·강변역" + }, + { + "routeID": 104900004, + "busRouteName": "광진05", + "routeType": "마을", + "startStation": "신자초등학교", + "endStation": "테크노마트앞.강변역" + }, + { + "routeID": 116900013, + "busRouteName": "구로01", + "routeType": "마을", + "startStation": "개봉역", + "endStation": "학마을2단지아파트" + }, + { + "routeID": 116900014, + "busRouteName": "구로02", + "routeType": "마을", + "startStation": "개봉역", + "endStation": "개봉중학교" + }, + { + "routeID": 116900010, + "busRouteName": "구로03", + "routeType": "마을", + "startStation": "개봉한진아파트", + "endStation": "개봉역·임괄아파트" + }, + { + "routeID": 116900011, + "busRouteName": "구로04", + "routeType": "마을", + "startStation": "천왕이펜하우스5단지", + "endStation": "현대2차APT" + }, + { + "routeID": 116900009, + "busRouteName": "구로05", + "routeType": "마을", + "startStation": "오류중학교", + "endStation": "개봉역" + }, + { + "routeID": 116900012, + "busRouteName": "구로06", + "routeType": "마을", + "startStation": "개봉역", + "endStation": "마젤란아파트·예일학원" + }, + { + "routeID": 116900006, + "busRouteName": "구로07", + "routeType": "마을", + "startStation": "오류동역", + "endStation": "항동우남퍼스트빌" + }, + { + "routeID": 116900015, + "busRouteName": "구로08", + "routeType": "마을", + "startStation": "개봉역", + "endStation": "오류동역" + }, + { + "routeID": 116900007, + "busRouteName": "구로09", + "routeType": "마을", + "startStation": "신도림동아1차아파트", + "endStation": "구로디지털단지역" + }, + { + "routeID": 116900003, + "busRouteName": "구로10", + "routeType": "마을", + "startStation": "대림역", + "endStation": "하이츠.영화.우방.한국현대아파트" + }, + { + "routeID": 116900004, + "busRouteName": "구로11", + "routeType": "마을", + "startStation": "대림역", + "endStation": "개봉역·임괄아파트" + }, + { + "routeID": 116900002, + "busRouteName": "구로12", + "routeType": "마을", + "startStation": "개봉역·임괄아파트", + "endStation": "거성아파트" + }, + { + "routeID": 116900001, + "busRouteName": "구로13", + "routeType": "마을", + "startStation": "구로1동주민센터.연예인아파트", + "endStation": "구로중학교" + }, + { + "routeID": 116900005, + "busRouteName": "구로14", + "routeType": "마을", + "startStation": "천왕이펜하우스5단지", + "endStation": "온수역" + }, + { + "routeID": 116900016, + "busRouteName": "구로15", + "routeType": "마을", + "startStation": "오류동역", + "endStation": "서울남부교정시설" + }, + { + "routeID": 117900008, + "busRouteName": "금천01", + "routeType": "마을", + "startStation": "독산역", + "endStation": "호압사입구" + }, + { + "routeID": 117900006, + "busRouteName": "금천02", + "routeType": "마을", + "startStation": "벽산3단지", + "endStation": "하안주공14단지" + }, + { + "routeID": 117900003, + "busRouteName": "금천03", + "routeType": "마을", + "startStation": "가산디지털단지역", + "endStation": "구로디지털단지역환승센터" + }, + { + "routeID": 117900002, + "busRouteName": "금천04", + "routeType": "마을", + "startStation": "성채안로입구삼거리", + "endStation": "금천구종합청사·금천구청역" + }, + { + "routeID": 117900001, + "busRouteName": "금천05", + "routeType": "마을", + "startStation": "독산한신코아·한신아파트", + "endStation": "KCC웰츠밸리" + }, + { + "routeID": 117900004, + "busRouteName": "금천06", + "routeType": "마을", + "startStation": "시흥1동공영주차장·금천노인종합복지관", + "endStation": "구로디지털단지역환승센터" + }, + { + "routeID": 117900007, + "busRouteName": "금천07", + "routeType": "마을", + "startStation": "시흥1동공영주차장·금천노인종합복지관", + "endStation": "구로디지털단지역환승센터" + }, + { + "routeID": 117900005, + "busRouteName": "금천08", + "routeType": "마을", + "startStation": "난곡중학교", + "endStation": "금천구종합청사·금천구청역" + }, + { + "routeID": 117900010, + "busRouteName": "금천11", + "routeType": "마을", + "startStation": "산기슭공원", + "endStation": "석수역" + }, + { + "routeID": 110900004, + "busRouteName": "노원01", + "routeType": "마을", + "startStation": "보람사거리", + "endStation": "상계역" + }, + { + "routeID": 110900009, + "busRouteName": "노원02", + "routeType": "마을", + "startStation": "수락리버시티", + "endStation": "노원구청" + }, + { + "routeID": 110900005, + "busRouteName": "노원03", + "routeType": "마을", + "startStation": "석계역", + "endStation": "하계역" + }, + { + "routeID": 110900001, + "busRouteName": "노원04", + "routeType": "마을", + "startStation": "석계역", + "endStation": "현대아파트" + }, + { + "routeID": 110900003, + "busRouteName": "노원05", + "routeType": "마을", + "startStation": "온곡초등학교", + "endStation": "창동역동측" + }, + { + "routeID": 110900007, + "busRouteName": "노원08", + "routeType": "마을", + "startStation": "상계8동주민센터", + "endStation": "창동역동측" + }, + { + "routeID": 110900006, + "busRouteName": "노원09", + "routeType": "마을", + "startStation": "서울특별시북부기술교육원", + "endStation": "동신아파트" + }, + { + "routeID": 110900002, + "busRouteName": "노원11", + "routeType": "마을", + "startStation": "수락현대아파트", + "endStation": "상계역" + }, + { + "routeID": 110900008, + "busRouteName": "노원13", + "routeType": "마을", + "startStation": "석계역", + "endStation": "서울과학기술대 붕어방" + }, + { + "routeID": 109900011, + "busRouteName": "노원14", + "routeType": "마을", + "startStation": "청백아파트1단지", + "endStation": "세그루학원" + }, + { + "routeID": 109900010, + "busRouteName": "노원15", + "routeType": "마을", + "startStation": "청백아파트1단지", + "endStation": "덕성여대입구" + }, + { + "routeID": 109900001, + "busRouteName": "도봉01", + "routeType": "마을", + "startStation": "법종사", + "endStation": "창동역동측" + }, + { + "routeID": 108900010, + "busRouteName": "도봉02", + "routeType": "마을", + "startStation": "우이동도선사입구.북한산우이역", + "endStation": "수유역.강북구청" + }, + { + "routeID": 109900007, + "busRouteName": "도봉03", + "routeType": "마을", + "startStation": "꽃동네", + "endStation": "수유역. 강북구청" + }, + { + "routeID": 109900008, + "busRouteName": "도봉04", + "routeType": "마을", + "startStation": "동익아파트", + "endStation": "수유역.강북구청" + }, + { + "routeID": 109900004, + "busRouteName": "도봉05", + "routeType": "마을", + "startStation": "한일병원", + "endStation": "우이동성원아파트" + }, + { + "routeID": 109900003, + "busRouteName": "도봉06", + "routeType": "마을", + "startStation": "한일병원", + "endStation": "방학3동주민센터" + }, + { + "routeID": 109900002, + "busRouteName": "도봉07", + "routeType": "마을", + "startStation": "도봉여성센터", + "endStation": "창2동건영아파트" + }, + { + "routeID": 109900006, + "busRouteName": "도봉08", + "routeType": "마을", + "startStation": "창동역동측", + "endStation": "무수골" + }, + { + "routeID": 109900005, + "busRouteName": "도봉09", + "routeType": "마을", + "startStation": "창동역동측", + "endStation": "도봉산역" + }, + { + "routeID": 105900003, + "busRouteName": "동대문01", + "routeType": "마을", + "startStation": "회기역", + "endStation": "경희의료원" + }, + { + "routeID": 105900002, + "busRouteName": "동대문02", + "routeType": "마을", + "startStation": "회기역", + "endStation": "외대앞역" + }, + { + "routeID": 105900006, + "busRouteName": "동대문03", + "routeType": "마을", + "startStation": "한국산업인력공단·전동중학교·서울가든아파트", + "endStation": "회기역" + }, + { + "routeID": 105900001, + "busRouteName": "동대문05", + "routeType": "마을", + "startStation": "답십리역", + "endStation": "영휘원사거리·(구)홍릉사거리" + }, + { + "routeID": 119900007, + "busRouteName": "동작01", + "routeType": "마을", + "startStation": "달마사", + "endStation": "대방역" + }, + { + "routeID": 119900013, + "busRouteName": "동작02", + "routeType": "마을", + "startStation": "사자암", + "endStation": "사자암" + }, + { + "routeID": 119900020, + "busRouteName": "동작03", + "routeType": "마을", + "startStation": "신대방삼거리역", + "endStation": "노들역" + }, + { + "routeID": 119900022, + "busRouteName": "동작05", + "routeType": "마을", + "startStation": "대방역", + "endStation": "신대방역" + }, + { + "routeID": 119900023, + "busRouteName": "동작05-1", + "routeType": "마을", + "startStation": "대방역", + "endStation": "신대방역" + }, + { + "routeID": 119900014, + "busRouteName": "동작06", + "routeType": "마을", + "startStation": "이수역", + "endStation": "사랑의병원" + }, + { + "routeID": 119900018, + "busRouteName": "동작07", + "routeType": "마을", + "startStation": "동작삼성래미안·롯데캐슬", + "endStation": "정금마을" + }, + { + "routeID": 119900009, + "busRouteName": "동작08", + "routeType": "마을", + "startStation": "행복유치원", + "endStation": "대방역" + }, + { + "routeID": 119900019, + "busRouteName": "동작09", + "routeType": "마을", + "startStation": "사당3동주민센터·대림현대아파트", + "endStation": "사당역" + }, + { + "routeID": 119900006, + "busRouteName": "동작10", + "routeType": "마을", + "startStation": "노들역·노량진교회", + "endStation": "상도역" + }, + { + "routeID": 119900011, + "busRouteName": "동작11", + "routeType": "마을", + "startStation": "사자암", + "endStation": "사자암" + }, + { + "routeID": 119900021, + "busRouteName": "동작12", + "routeType": "마을", + "startStation": "상도4동성문교회", + "endStation": "대방역" + }, + { + "routeID": 120900001, + "busRouteName": "동작13", + "routeType": "마을", + "startStation": "봉현초등학교", + "endStation": "대방역" + }, + { + "routeID": 119900012, + "busRouteName": "동작14", + "routeType": "마을", + "startStation": "달마사", + "endStation": "사랑의병원" + }, + { + "routeID": 119900010, + "busRouteName": "동작15", + "routeType": "마을", + "startStation": "자이아파트", + "endStation": "이수역" + }, + { + "routeID": 119900017, + "busRouteName": "동작16", + "routeType": "마을", + "startStation": "동작삼성래미안·롯데캐슬", + "endStation": "사당역" + }, + { + "routeID": 119900016, + "busRouteName": "동작17", + "routeType": "마을", + "startStation": "사당극동아파트상가", + "endStation": "이수힐스테이트아파트" + }, + { + "routeID": 119900015, + "busRouteName": "동작18", + "routeType": "마을", + "startStation": "사당극동아파트상가", + "endStation": "사당역" + }, + { + "routeID": 119900008, + "busRouteName": "동작19", + "routeType": "마을", + "startStation": "방범초소", + "endStation": "신대방삼거리역" + }, + { + "routeID": 119900001, + "busRouteName": "동작20", + "routeType": "마을", + "startStation": "자이아파트", + "endStation": "낙성대역" + }, + { + "routeID": 119900024, + "busRouteName": "동작21", + "routeType": "마을", + "startStation": "달마사", + "endStation": "상도역" + }, + { + "routeID": 102900001, + "busRouteName": "마포01", + "routeType": "마을", + "startStation": "산천동리버힐삼성아파트", + "endStation": "용문시장.새마을금고" + }, + { + "routeID": 113900005, + "busRouteName": "마포02", + "routeType": "마을", + "startStation": "청암대", + "endStation": "공덕역" + }, + { + "routeID": 113900002, + "busRouteName": "마포03", + "routeType": "마을", + "startStation": "아현역", + "endStation": "만리동고개" + }, + { + "routeID": 113900011, + "busRouteName": "마포05", + "routeType": "마을", + "startStation": "홍대입구역", + "endStation": "홍대입구역" + }, + { + "routeID": 113900012, + "busRouteName": "마포06", + "routeType": "마을", + "startStation": "홍대입구역", + "endStation": "남가좌현대아이파크" + }, + { + "routeID": 113900010, + "busRouteName": "마포07", + "routeType": "마을", + "startStation": "절두산순교기념관", + "endStation": "현대백화점" + }, + { + "routeID": 113900013, + "busRouteName": "마포08", + "routeType": "마을", + "startStation": "월드컵파크7단지", + "endStation": "신촌역" + }, + { + "routeID": 113900015, + "busRouteName": "마포09", + "routeType": "마을", + "startStation": "망원유수지·마포구민체육센터", + "endStation": "금호태영아파트" + }, + { + "routeID": 113900008, + "busRouteName": "마포10", + "routeType": "마을", + "startStation": "신촌역·이마트신촌점", + "endStation": "아현동주민센터" + }, + { + "routeID": 113900004, + "busRouteName": "마포11", + "routeType": "마을", + "startStation": "신촌역·이마트신촌점", + "endStation": "염리초등학교" + }, + { + "routeID": 113900003, + "busRouteName": "마포12", + "routeType": "마을", + "startStation": "신촌역·이마트신촌점", + "endStation": "마포역" + }, + { + "routeID": 113900007, + "busRouteName": "마포13", + "routeType": "마을", + "startStation": "창전중앙하이츠아파트", + "endStation": "신촌역" + }, + { + "routeID": 113900006, + "busRouteName": "마포14", + "routeType": "마을", + "startStation": "창전삼성아파트 110동", + "endStation": "경의선책거리·산울림소극장" + }, + { + "routeID": 113900014, + "busRouteName": "마포15", + "routeType": "마을", + "startStation": "월드컵파크7단지", + "endStation": "홍대입구역" + }, + { + "routeID": 113900009, + "busRouteName": "마포16", + "routeType": "마을", + "startStation": "진선주택", + "endStation": "서교푸르지오아파트" + }, + { + "routeID": 113900001, + "busRouteName": "마포17", + "routeType": "마을", + "startStation": "공덕시장", + "endStation": "공덕시장" + }, + { + "routeID": 113900016, + "busRouteName": "마포18", + "routeType": "마을", + "startStation": "월드컵파크6,7단지", + "endStation": "디지털미디어시티역" + }, + { + "routeID": 113900017, + "busRouteName": "마포18-1", + "routeType": "마을", + "startStation": "월드컵파크6,7단지", + "endStation": "상암월드컵파크6.7단지" + }, + { + "routeID": 112900010, + "busRouteName": "서대문01", + "routeType": "마을", + "startStation": "삼성빌라", + "endStation": "홍제역" + }, + { + "routeID": 112900014, + "busRouteName": "서대문02대", + "routeType": "마을", + "startStation": "뜨란채아파트101동", + "endStation": "극동아파트119동" + }, + { + "routeID": 112900015, + "busRouteName": "서대문02소", + "routeType": "마을", + "startStation": "뜨란채아파트101동", + "endStation": "뜨란채아파트101동" + }, + { + "routeID": 112900004, + "busRouteName": "서대문03", + "routeType": "마을", + "startStation": "홍은2동주민센터", + "endStation": "신촌오거리·신촌역" + }, + { + "routeID": 112900011, + "busRouteName": "서대문04", + "routeType": "마을", + "startStation": "궁동공원", + "endStation": "신촌오거리·신촌역" + }, + { + "routeID": 112900009, + "busRouteName": "서대문05", + "routeType": "마을", + "startStation": "북아현삼거리", + "endStation": "신촌오거리·신촌역" + }, + { + "routeID": 112900007, + "busRouteName": "서대문06", + "routeType": "마을", + "startStation": "두산아파트", + "endStation": "서울역서부" + }, + { + "routeID": 112900003, + "busRouteName": "서대문07", + "routeType": "마을", + "startStation": "개미마을", + "endStation": "홍제역" + }, + { + "routeID": 100900012, + "busRouteName": "서대문08", + "routeType": "마을", + "startStation": "상명대정문", + "endStation": "홍제역" + }, + { + "routeID": 112900012, + "busRouteName": "서대문09대", + "routeType": "마을", + "startStation": "가산빌라", + "endStation": "홍제역" + }, + { + "routeID": 112900013, + "busRouteName": "서대문09소", + "routeType": "마을", + "startStation": "동남슈퍼", + "endStation": "홍제역" + }, + { + "routeID": 112900001, + "busRouteName": "서대문10", + "routeType": "마을", + "startStation": "백련사", + "endStation": "홍제역" + }, + { + "routeID": 112900006, + "busRouteName": "서대문11", + "routeType": "마을", + "startStation": "홍은동국민주택", + "endStation": "봉원사입구" + }, + { + "routeID": 112900002, + "busRouteName": "서대문12", + "routeType": "마을", + "startStation": "동원베네스트아파트", + "endStation": "홍제역" + }, + { + "routeID": 112900005, + "busRouteName": "서대문13", + "routeType": "마을", + "startStation": "홍은동국민주택", + "endStation": "인왕산현대아파트" + }, + { + "routeID": 112900008, + "busRouteName": "서대문14", + "routeType": "마을", + "startStation": "구.정원슈퍼", + "endStation": "구.정원슈퍼" + }, + { + "routeID": 108900014, + "busRouteName": "서대문15", + "routeType": "마을", + "startStation": "증산역.증산2교", + "endStation": "명지대후문" + }, + { + "routeID": 121900009, + "busRouteName": "서초01", + "routeType": "마을", + "startStation": "매일상가", + "endStation": "반포고교" + }, + { + "routeID": 121900008, + "busRouteName": "서초02", + "routeType": "마을", + "startStation": "교대역", + "endStation": "서초역" + }, + { + "routeID": 121900005, + "busRouteName": "서초03", + "routeType": "마을", + "startStation": "교대역", + "endStation": "신사사거리·가로수길" + }, + { + "routeID": 119900003, + "busRouteName": "서초05", + "routeType": "마을", + "startStation": "정금마을", + "endStation": "세탁소" + }, + { + "routeID": 119900002, + "busRouteName": "서초06", + "routeType": "마을", + "startStation": "정금마을", + "endStation": "동덕여고" + }, + { + "routeID": 121900011, + "busRouteName": "서초07", + "routeType": "마을", + "startStation": "황실자이아파트", + "endStation": "임광아파트" + }, + { + "routeID": 121900002, + "busRouteName": "서초08", + "routeType": "마을", + "startStation": "서울추모공원", + "endStation": "양재역" + }, + { + "routeID": 121900007, + "busRouteName": "서초09", + "routeType": "마을", + "startStation": "헌인가구단지", + "endStation": "강남역" + }, + { + "routeID": 121900003, + "busRouteName": "서초10", + "routeType": "마을", + "startStation": "서초구민체육센터", + "endStation": "강남역" + }, + { + "routeID": 121900006, + "busRouteName": "서초11", + "routeType": "마을", + "startStation": "예술의전당", + "endStation": "반포1동성당" + }, + { + "routeID": 121900013, + "busRouteName": "서초13", + "routeType": "마을", + "startStation": "이수역", + "endStation": "동덕여고" + }, + { + "routeID": 121900010, + "busRouteName": "서초14", + "routeType": "마을", + "startStation": "이수역", + "endStation": "경원중학교" + }, + { + "routeID": 121900012, + "busRouteName": "서초15", + "routeType": "마을", + "startStation": "롯데캐슬헤론아파트", + "endStation": "사당역" + }, + { + "routeID": 119900005, + "busRouteName": "서초16", + "routeType": "마을", + "startStation": "정금마을", + "endStation": "방배역" + }, + { + "routeID": 119900004, + "busRouteName": "서초17", + "routeType": "마을", + "startStation": "사당역", + "endStation": "서초구청" + }, + { + "routeID": 121900016, + "busRouteName": "서초18", + "routeType": "마을", + "startStation": "선바위역", + "endStation": "양재근린공원" + }, + { + "routeID": 121900015, + "busRouteName": "서초18-1", + "routeType": "마을", + "startStation": "LH5단지", + "endStation": "양재역" + }, + { + "routeID": 121900004, + "busRouteName": "서초20", + "routeType": "마을", + "startStation": "서초더샵포레", + "endStation": "양재역" + }, + { + "routeID": 121900001, + "busRouteName": "서초21", + "routeType": "마을", + "startStation": "염곡공영차고지", + "endStation": "방배신삼호아파트" + }, + { + "routeID": 121900014, + "busRouteName": "서초22", + "routeType": "마을", + "startStation": "서울남부터미널", + "endStation": "아리랑TV·국립국악원" + }, + { + "routeID": 103900003, + "busRouteName": "성동01", + "routeType": "마을", + "startStation": "옥수역", + "endStation": "신당역·하나은행" + }, + { + "routeID": 103900004, + "busRouteName": "성동02", + "routeType": "마을", + "startStation": "마장동현대아파트", + "endStation": "논골·금호벽산아파트·성동공유센터" + }, + { + "routeID": 103900011, + "busRouteName": "성동03-1", + "routeType": "마을", + "startStation": "신금호역", + "endStation": "청계천박물관" + }, + { + "routeID": 103900012, + "busRouteName": "성동03-2", + "routeType": "마을", + "startStation": "왕십리KCC스위첸아파트", + "endStation": "한양대학교생활과학대학" + }, + { + "routeID": 101900001, + "busRouteName": "성동05", + "routeType": "마을", + "startStation": "응봉근린공원·금호산", + "endStation": "약수역" + }, + { + "routeID": 103900007, + "busRouteName": "성동06", + "routeType": "마을", + "startStation": "두산아파트115동", + "endStation": "금남시장·화단앞" + }, + { + "routeID": 103900005, + "busRouteName": "성동07", + "routeType": "마을", + "startStation": "대우아파트관리실", + "endStation": "약수역" + }, + { + "routeID": 103900002, + "busRouteName": "성동08", + "routeType": "마을", + "startStation": "금호역", + "endStation": "청계벽산아파트.텐즈힐아파트" + }, + { + "routeID": 103900001, + "busRouteName": "성동09", + "routeType": "마을", + "startStation": "옥수역", + "endStation": "극동아파트5동" + }, + { + "routeID": 103900008, + "busRouteName": "성동10", + "routeType": "마을", + "startStation": "경동초등학교입구", + "endStation": "송정동서울숲아이파크" + }, + { + "routeID": 103900009, + "busRouteName": "성동12", + "routeType": "마을", + "startStation": "약수지구대", + "endStation": "옥수역" + }, + { + "routeID": 103900010, + "busRouteName": "성동13", + "routeType": "마을", + "startStation": "옥수역", + "endStation": "성수역3번출구" + }, + { + "routeID": 103900013, + "busRouteName": "성동14", + "routeType": "마을", + "startStation": "응봉역·광희중학교", + "endStation": "왕십리역" + }, + { + "routeID": 107900008, + "busRouteName": "성북01", + "routeType": "마을", + "startStation": "동구마케팅고후문", + "endStation": "삼선동주민센터" + }, + { + "routeID": 107900003, + "busRouteName": "성북02", + "routeType": "마을", + "startStation": "한성대정문", + "endStation": "우리옛돌박물관·정법사" + }, + { + "routeID": 107900002, + "busRouteName": "성북03", + "routeType": "마을", + "startStation": "삼선초교", + "endStation": "노인정" + }, + { + "routeID": 107900005, + "busRouteName": "성북04", + "routeType": "마을", + "startStation": "성신여대입구역", + "endStation": "신설동역" + }, + { + "routeID": 107900011, + "busRouteName": "성북05", + "routeType": "마을", + "startStation": "정릉2동주민센터", + "endStation": "정수빌라" + }, + { + "routeID": 107900012, + "busRouteName": "성북06", + "routeType": "마을", + "startStation": "산림초소", + "endStation": "길음역" + }, + { + "routeID": 107900013, + "busRouteName": "성북07", + "routeType": "마을", + "startStation": "정릉4동종점", + "endStation": "길음역" + }, + { + "routeID": 107900007, + "busRouteName": "성북08", + "routeType": "마을", + "startStation": "신안아파트·길음초등학교", + "endStation": "길음역" + }, + { + "routeID": 107900006, + "busRouteName": "성북09", + "routeType": "마을", + "startStation": "신안아파트·길음초등학교", + "endStation": "미아사거리" + }, + { + "routeID": 107900014, + "busRouteName": "성북10-1", + "routeType": "마을", + "startStation": "장위중학교", + "endStation": "미아사거리역" + }, + { + "routeID": 107900017, + "busRouteName": "성북10-2", + "routeType": "마을", + "startStation": "우남아파트", + "endStation": "미아사거리역" + }, + { + "routeID": 107900004, + "busRouteName": "성북12", + "routeType": "마을", + "startStation": "월곡중학교", + "endStation": "석계역" + }, + { + "routeID": 107900001, + "busRouteName": "성북13", + "routeType": "마을", + "startStation": "동방주택", + "endStation": "석계역" + }, + { + "routeID": 107900010, + "busRouteName": "성북14-1", + "routeType": "마을", + "startStation": "북서울꿈의숲", + "endStation": "석계역문화공원" + }, + { + "routeID": 107900016, + "busRouteName": "성북14-2", + "routeType": "마을", + "startStation": "장곡시장", + "endStation": "석계역" + }, + { + "routeID": 107900015, + "busRouteName": "성북15", + "routeType": "마을", + "startStation": "고려아파트", + "endStation": "한미약국" + }, + { + "routeID": 105900004, + "busRouteName": "성북20", + "routeType": "마을", + "startStation": "고려대역 3번출구", + "endStation": "성신여대입구역" + }, + { + "routeID": 105900005, + "busRouteName": "성북21", + "routeType": "마을", + "startStation": "고려대역 3번출구", + "endStation": "길음역" + }, + { + "routeID": 107900009, + "busRouteName": "성북22", + "routeType": "마을", + "startStation": "정릉", + "endStation": "삼선동주민센터" + }, + { + "routeID": 114900001, + "busRouteName": "양천01", + "routeType": "마을", + "startStation": "등촌역.강서보건소", + "endStation": "당산역" + }, + { + "routeID": 114900002, + "busRouteName": "양천02", + "routeType": "마을", + "startStation": "등촌역·강서보건소", + "endStation": "오목교역.대학학원" + }, + { + "routeID": 114900003, + "busRouteName": "양천03", + "routeType": "마을", + "startStation": "신정이펜하우스1단지", + "endStation": "목동역" + }, + { + "routeID": 116900018, + "busRouteName": "양천04", + "routeType": "마을", + "startStation": "신도림동아3차아파트", + "endStation": "양천구청역" + }, + { + "routeID": 116900008, + "busRouteName": "영등포01", + "routeType": "마을", + "startStation": "신도림역", + "endStation": "구로디지털단지역" + }, + { + "routeID": 118900004, + "busRouteName": "영등포02", + "routeType": "마을", + "startStation": "한국수자원연구소", + "endStation": "신동아아파트·관악고교" + }, + { + "routeID": 118900005, + "busRouteName": "영등포03", + "routeType": "마을", + "startStation": "현대2·3차아파트", + "endStation": "영등포시장·우리은행" + }, + { + "routeID": 118900001, + "busRouteName": "영등포04", + "routeType": "마을", + "startStation": "대림역", + "endStation": "경방타임스퀘어·신세계백화점" + }, + { + "routeID": 118900006, + "busRouteName": "영등포05", + "routeType": "마을", + "startStation": "당산역", + "endStation": "영등포역" + }, + { + "routeID": 118900003, + "busRouteName": "영등포06", + "routeType": "마을", + "startStation": "대방역", + "endStation": "영등포역후문" + }, + { + "routeID": 118900002, + "busRouteName": "영등포07", + "routeType": "마을", + "startStation": "대방역", + "endStation": "신길5동주민센터" + }, + { + "routeID": 118900007, + "busRouteName": "영등포08", + "routeType": "마을", + "startStation": "영등포푸르지오후문", + "endStation": "신도림역" + }, + { + "routeID": 116900017, + "busRouteName": "영등포09", + "routeType": "마을", + "startStation": "신도림역", + "endStation": "영등포역후문" + }, + { + "routeID": 118900008, + "busRouteName": "영등포10", + "routeType": "마을", + "startStation": "대방역", + "endStation": "대방역" + }, + { + "routeID": 118900009, + "busRouteName": "영등포11", + "routeType": "마을", + "startStation": "대방역", + "endStation": "대방역" + }, + { + "routeID": 114900006, + "busRouteName": "영등포12", + "routeType": "마을", + "startStation": "신도림역", + "endStation": "당산역" + }, + { + "routeID": 114900005, + "busRouteName": "영등포13", + "routeType": "마을", + "startStation": "대방천사거리", + "endStation": "신도림역" + }, + { + "routeID": 102900003, + "busRouteName": "용산01", + "routeType": "마을", + "startStation": "순천향대병원", + "endStation": "순천향대병원" + }, + { + "routeID": 102900002, + "busRouteName": "용산02", + "routeType": "마을", + "startStation": "녹사평역", + "endStation": "남영역" + }, + { + "routeID": 102900004, + "busRouteName": "용산03", + "routeType": "마을", + "startStation": "하얏트호텔", + "endStation": "용산문화체육센터" + }, + { + "routeID": 101900002, + "busRouteName": "용산04", + "routeType": "마을", + "startStation": "서울역서부", + "endStation": "남정초등학교" + }, + { + "routeID": 111900002, + "busRouteName": "은평01", + "routeType": "마을", + "startStation": "연신내역", + "endStation": "석광사" + }, + { + "routeID": 111900003, + "busRouteName": "은평02", + "routeType": "마을", + "startStation": "두산·한신아파트", + "endStation": "불광역" + }, + { + "routeID": 111900001, + "busRouteName": "은평03", + "routeType": "마을", + "startStation": "갈현건영아파트", + "endStation": "연신내역" + }, + { + "routeID": 111900004, + "busRouteName": "은평04", + "routeType": "마을", + "startStation": "경향파크아파트", + "endStation": "불광역" + }, + { + "routeID": 111900005, + "busRouteName": "은평05", + "routeType": "마을", + "startStation": "백련산힐스테이트302동", + "endStation": "녹번역" + }, + { + "routeID": 111900007, + "busRouteName": "은평06", + "routeType": "마을", + "startStation": "불광동팀수양관", + "endStation": "대성중·고등학교" + }, + { + "routeID": 111900006, + "busRouteName": "은평07", + "routeType": "마을", + "startStation": "불광삼성래미안아파트", + "endStation": "불광역" + }, + { + "routeID": 111900011, + "busRouteName": "은평08-1", + "routeType": "마을", + "startStation": "백련산힐스테이트302동", + "endStation": "새절역.숭실고입구" + }, + { + "routeID": 111900012, + "busRouteName": "은평08-2", + "routeType": "마을", + "startStation": "백련산힐스테이트302동", + "endStation": "새절역.숭실고입구" + }, + { + "routeID": 111900009, + "busRouteName": "은평09", + "routeType": "마을", + "startStation": "갈현e편한세상1단지", + "endStation": "청구성심병원" + }, + { + "routeID": 111900010, + "busRouteName": "은평10", + "routeType": "마을", + "startStation": "숭실고등학교정문", + "endStation": "보람슈퍼·산새마을종점" + }, + { + "routeID": 100900006, + "busRouteName": "종로01", + "routeType": "마을", + "startStation": "빨래터·고희동미술관", + "endStation": "종로사우나" + }, + { + "routeID": 100900008, + "busRouteName": "종로02", + "routeType": "마을", + "startStation": "성균관대학교후문", + "endStation": "종각역.YMCA" + }, + { + "routeID": 100900010, + "busRouteName": "종로03", + "routeType": "마을", + "startStation": "낙산공원", + "endStation": "종로5가역" + }, + { + "routeID": 100900011, + "busRouteName": "종로05", + "routeType": "마을", + "startStation": "서대문역3번출구", + "endStation": "서대문역3번출구" + }, + { + "routeID": 100900004, + "busRouteName": "종로07", + "routeType": "마을", + "startStation": "명륜새마을금고", + "endStation": "방송통신대" + }, + { + "routeID": 100900005, + "busRouteName": "종로08", + "routeType": "마을", + "startStation": "명륜3가종점", + "endStation": "종로5가역" + }, + { + "routeID": 100900003, + "busRouteName": "종로09", + "routeType": "마을", + "startStation": "수성동계곡", + "endStation": "시청역" + }, + { + "routeID": 100900007, + "busRouteName": "종로11", + "routeType": "마을", + "startStation": "삼청공원", + "endStation": "서울역" + }, + { + "routeID": 100900009, + "busRouteName": "종로12", + "routeType": "마을", + "startStation": "서울대학교병원", + "endStation": "서울대학교병원" + }, + { + "routeID": 100900002, + "busRouteName": "종로13", + "routeType": "마을", + "startStation": "평창동주민센터", + "endStation": "부암동주민센터·무계원" + }, + { + "routeID": 106900001, + "busRouteName": "중랑01", + "routeType": "마을", + "startStation": "중화동종점", + "endStation": "신이문역" + }, + { + "routeID": 106900002, + "busRouteName": "중랑02", + "routeType": "마을", + "startStation": "진로아파트종점", + "endStation": "한신아파트" + } ] diff --git a/BBus/BBus/Global/UseCase/AverageSectionTimeCalculatable.swift b/BBus/BBus/Global/UseCase/AverageSectionTimeCalculatable.swift new file mode 100644 index 00000000..d85d3530 --- /dev/null +++ b/BBus/BBus/Global/UseCase/AverageSectionTimeCalculatable.swift @@ -0,0 +1,22 @@ +// +// AverageSectionTimeCalculatable.swift +// BBus +// +// Created by Kang Minsang on 2021/11/30. +// + +import Foundation + +protocol AverageSectionTimeCalculatable: BaseUseCase { + func averageSectionTime(speed: Int, distance: Int) -> Int +} + +extension AverageSectionTimeCalculatable { + func averageSectionTime(speed: Int, distance: Int) -> Int { + let averageBusSpeed: Double = 21 + let metterToKilometter: Double = 0.06 + + let result = Double(distance)/averageBusSpeed*metterToKilometter + return Int(ceil(result)) + } +} diff --git a/BBus/BBus/Info.plist b/BBus/BBus/Info.plist index ed7b94e4..92ca64c5 100644 --- a/BBus/BBus/Info.plist +++ b/BBus/BBus/Info.plist @@ -2,30 +2,18 @@ - NSLocationUsageDescription - 하차 알람을 위해서 사용자의 GPS 위치를 추적해야합니다. - NSLocationWhenInUseUsageDescription - 알람을 위해 사용자의 GPS 위치를 추적해야합니다. NSLocationAlwaysAndWhenInUseUsageDescription 알람을 위해 앱을 사용중이지 않을 때도 사용자의 GPS 위치를 추적해야합니다. + NSLocationWhenInUseUsageDescription + 알람을 위해 사용자의 GPS 위치를 추적해야합니다. + NSLocationUsageDescription + 하차 알람을 위해서 사용자의 GPS 위치를 추적해야합니다. + UISupportedInterfaceOrientations~iphone + + UIInterfaceOrientationPortrait + API_ACCESS_KEY1 ${API_ACCESS_KEY1} - API_ACCESS_KEY2 - ${API_ACCESS_KEY2} - API_ACCESS_KEY3 - ${API_ACCESS_KEY3} - API_ACCESS_KEY4 - ${API_ACCESS_KEY4} - API_ACCESS_KEY5 - ${API_ACCESS_KEY5} - API_ACCESS_KEY6 - ${API_ACCESS_KEY6} - API_ACCESS_KEY7 - ${API_ACCESS_KEY7} - API_ACCESS_KEY8 - ${API_ACCESS_KEY8} - API_ACCESS_KEY9 - ${API_ACCESS_KEY9} API_ACCESS_KEY10 ${API_ACCESS_KEY10} API_ACCESS_KEY11 @@ -42,6 +30,22 @@ ${API_ACCESS_KEY16} API_ACCESS_KEY17 ${API_ACCESS_KEY17} + API_ACCESS_KEY2 + ${API_ACCESS_KEY2} + API_ACCESS_KEY3 + ${API_ACCESS_KEY3} + API_ACCESS_KEY4 + ${API_ACCESS_KEY4} + API_ACCESS_KEY5 + ${API_ACCESS_KEY5} + API_ACCESS_KEY6 + ${API_ACCESS_KEY6} + API_ACCESS_KEY7 + ${API_ACCESS_KEY7} + API_ACCESS_KEY8 + ${API_ACCESS_KEY8} + API_ACCESS_KEY9 + ${API_ACCESS_KEY9} NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/BBus/BusRouteViewModelTests/BusRouteViewModelTests.swift b/BBus/BusRouteViewModelTests/BusRouteViewModelTests.swift new file mode 100644 index 00000000..eb96290c --- /dev/null +++ b/BBus/BusRouteViewModelTests/BusRouteViewModelTests.swift @@ -0,0 +1,195 @@ +// +// BusRouteViewModelTests.swift +// BusRouteViewModelTests +// +// Created by Kang Minsang on 2021/12/01. +// + +import XCTest +import Foundation +import Combine + +class BusRouteViewModelTests: XCTestCase { + + var busRouteViewModel: BusRouteViewModel? + var cancellables: Set = [] + + class DummyBusRouteAPIUseCase: BusRouteAPIUsable { + func searchHeader(busRouteId: Int) -> AnyPublisher { + let dummyDTO = BusRouteDTO(routeID: 100100260, + busRouteName: "5524", + routeType: .localLine, + startStation: "난향차고지", + endStation: "중앙대학교") + return Just(dummyDTO).setFailureType(to: Error.self).eraseToAnyPublisher() + } + + func fetchRouteList(busRouteId: Int) -> AnyPublisher<[StationByRouteListDTO], Error> { + let dummyStation1 = StationByRouteListDTO(sectionSpeed: 0, + sequence: 1, + stationName: "난곡종점", + fullSectionDistance: 0, + arsId: "21809", + beginTm: "04:00", + lastTm: "22:30", + transYn: "N") + let dummyStation2 = StationByRouteListDTO(sectionSpeed: 44, + sequence: 2, + stationName: "신림복지관앞", + fullSectionDistance: 247, + arsId: "21211", + beginTm: "04:00", + lastTm: "00:18", + transYn: "N") + let dummyStation3 = StationByRouteListDTO(sectionSpeed: 29, + sequence: 3, + stationName: "난우중학교입구", + fullSectionDistance: 190, + arsId: "21210", + beginTm: "04:00", + lastTm: "22:30", + transYn: "N") + return Just([dummyStation1, dummyStation2, dummyStation3]).setFailureType(to: Error.self).eraseToAnyPublisher() + } + + func fetchBusPosList(busRouteId: Int) -> AnyPublisher<[BusPosByRtidDTO], Error> { + let dummyBus1 = BusPosByRtidDTO(busType: 1, + congestion: 0, + plainNumber: "서울74사5255", + sectionOrder: 22, + fullSectDist: "0.351", + sectDist: "0", + gpsY: 37.4893, + gpsX: 126.927062) + let dummyBus2 = BusPosByRtidDTO(busType: 1, + congestion: 0, + plainNumber: "서울74사5254", + sectionOrder: 28, + fullSectDist: "0.378", + sectDist: "0.017", + gpsY: 37.486795, + gpsX: 126.947757) + let dummyBus3 = BusPosByRtidDTO(busType: 1, + congestion: 0, + plainNumber: "서울74사5252", + sectionOrder: 32, + fullSectDist: "0.41", + sectDist: "0.022", + gpsY: 37.48311, + gpsX: 126.954122) + return Just([dummyBus1, dummyBus2, dummyBus3]).setFailureType(to: Error.self).eraseToAnyPublisher() + } + } + + override func setUpWithError() throws { + super.setUp() + self.busRouteViewModel = BusRouteViewModel(useCase: DummyBusRouteAPIUseCase(), busRouteId: 100100260) + } + + override func tearDownWithError() throws { + super.tearDown() + self.busRouteViewModel = nil + } + + func test_bindHeaderInfo_수신_성공() throws { + // given + guard let viewModel = self.busRouteViewModel else { + XCTFail("viewModel is nil") + return + } + let expectation = XCTestExpectation() + let answerDTO = BusRouteDTO(routeID: 100100260, + busRouteName: "5524", + routeType: .localLine, + startStation: "난향차고지", + endStation: "중앙대학교") + + // when + viewModel.$header + .receive(on: DispatchQueue.global()) + .sink { completion in + // then + guard case .failure(let error) = completion else { return } + XCTFail("\(error.localizedDescription)") + expectation.fulfill() + } receiveValue: { header in + guard let header = header else { return } + // then + XCTAssertEqual(header.routeID, answerDTO.routeID) + XCTAssertEqual(header.busRouteName, answerDTO.busRouteName) + XCTAssertEqual(header.routeType, answerDTO.routeType) + XCTAssertEqual(header.startStation, answerDTO.startStation) + XCTAssertEqual(header.endStation, answerDTO.endStation) + expectation.fulfill() + } + .store(in: &self.cancellables) + + wait(for: [expectation], timeout: 10) + } + + func test_bindBodysInfo_수신_성공() throws { + // given + guard let viewModel = self.busRouteViewModel else { + XCTFail("viewModel is nil") + return + } + let expectation = XCTestExpectation() + let station1 = BusStationInfo(speed: 0, afterSpeed: 44, count: 3, title: "난곡종점", description: "21809 | 04:00-22:30", transYn: "N", arsId: "21809") + let station2 = BusStationInfo(speed: 44, afterSpeed: 29, count: 3, title: "신림복지관앞", description: "21211 | 04:00-00:18", transYn: "N", arsId: "21211") + let station3 = BusStationInfo(speed: 29, afterSpeed: nil, count: 3, title: "난우중학교입구", description: "21210 | 04:00-22:30", transYn: "N", arsId: "21210") + let answerStations = [station1, station2, station3] + + // when + viewModel.$bodys + .receive(on: DispatchQueue.global()) + .filter({ !$0.isEmpty }) + .sink { completion in + // then + guard case .failure(let error) = completion else { return } + XCTFail("\(error.localizedDescription)") + expectation.fulfill() + } receiveValue: { bodys in + // then + XCTAssertEqual(bodys[0].arsId, answerStations[0].arsId) + XCTAssertEqual(bodys[1].description, answerStations[1].description) + XCTAssertEqual(bodys[2].afterSpeed, answerStations[2].afterSpeed) + expectation.fulfill() + } + .store(in: &self.cancellables) + + wait(for: [expectation], timeout: 10) + } + + func test_bindBusesPosInfo_수신_성공() throws { + // given + guard let viewModel = self.busRouteViewModel else { + XCTFail("viewModel is nil") + return + } + let expectation = XCTestExpectation() + let bus1 = BusPosInfo(location: CGFloat(21), number: "5255", congestion: .normal, islower: true) + let bus2 = BusPosInfo(location: CGFloat(27) + CGFloat(("0.017" as NSString).floatValue)/CGFloat(("0.378" as NSString).floatValue), number: "5254", congestion: .normal, islower: true) + let bus3 = BusPosInfo(location: CGFloat(31) + CGFloat(0.022)/CGFloat(0.41), number: "5252", congestion: .normal, islower: true) + let answerBuses = [bus1, bus2, bus3] + + // when + viewModel.$buses + .receive(on: DispatchQueue.global()) + .filter({ !$0.isEmpty }) + .sink { completion in + // then + guard case .failure(let error) = completion else { return } + XCTFail("\(error.localizedDescription)") + expectation.fulfill() + } receiveValue: { buses in + // then + XCTAssertEqual(buses[0].number, answerBuses[0].number) + XCTAssertEqual(buses[1].location, answerBuses[1].location) + XCTAssertEqual(buses[2].congestion, answerBuses[2].congestion) + expectation.fulfill() + } + .store(in: &self.cancellables) + + wait(for: [expectation], timeout: 10) + } +} diff --git a/BBus/HomeViewModelTests/HomeViewModelTests.swift b/BBus/HomeViewModelTests/HomeViewModelTests.swift new file mode 100644 index 00000000..a707a13f --- /dev/null +++ b/BBus/HomeViewModelTests/HomeViewModelTests.swift @@ -0,0 +1,32 @@ +// +// HomeViewModelTests.swift +// HomeViewModelTests +// +// Created by 김태훈 on 2021/11/30. +// + +import XCTest + +class HomeViewModelTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/BBus/MovingStatusViewModelTests/MovingStatusViewModelTests.swift b/BBus/MovingStatusViewModelTests/MovingStatusViewModelTests.swift new file mode 100644 index 00000000..554ef15c --- /dev/null +++ b/BBus/MovingStatusViewModelTests/MovingStatusViewModelTests.swift @@ -0,0 +1,271 @@ +// +// MovingStatusViewModelTests.swift +// MovingStatusViewModelTests +// +// Created by 김태훈 on 2021/11/30. +// + +import XCTest +import Foundation +import Combine + +class MovingStatusViewModelTests: XCTestCase { + + var cancellables: Set = [] + + class DummyMovingStatusAPIUseCase: MovingStatusAPIUsable { + func searchHeader(busRouteId: Int) -> AnyPublisher { + let dummyDTO = BusRouteDTO(routeID: 100100260, + busRouteName: "5524", + routeType: .localLine, + startStation: "난향차고지", + endStation: "중앙대학교") + return Just(dummyDTO).setFailureType(to: Error.self).eraseToAnyPublisher() + } + + func fetchRouteList(busRouteId: Int) -> AnyPublisher<[StationByRouteListDTO], Error> { + let dummyStation1 = StationByRouteListDTO(sectionSpeed: 0, + sequence: 1, + stationName: "난곡종점", + fullSectionDistance: 0, + arsId: "21809", + beginTm: "04:00", + lastTm: "22:30", + transYn: "N") + let dummyStation2 = StationByRouteListDTO(sectionSpeed: 44, + sequence: 2, + stationName: "신림복지관앞", + fullSectionDistance: 247, + arsId: "21211", + beginTm: "04:00", + lastTm: "00:18", + transYn: "N") + let dummyStation3 = StationByRouteListDTO(sectionSpeed: 29, + sequence: 3, + stationName: "난우중학교입구", + fullSectionDistance: 190, + arsId: "21210", + beginTm: "04:00", + lastTm: "22:30", + transYn: "N") + return Just([dummyStation1, dummyStation2, dummyStation3]).setFailureType(to: Error.self).eraseToAnyPublisher() + } + + func fetchBusPosList(busRouteId: Int) -> AnyPublisher<[BusPosByRtidDTO], Error> { + let dummyBus1 = BusPosByRtidDTO(busType: 1, + congestion: 0, + plainNumber: "서울74사5255", + sectionOrder: 0, + fullSectDist: "0.351", + sectDist: "0", + gpsY: 37.4893, + gpsX: 126.927062) + let dummyBus2 = BusPosByRtidDTO(busType: 1, + congestion: 0, + plainNumber: "서울74사5254", + sectionOrder: 2, + fullSectDist: "0.378", + sectDist: "0.017", + gpsY: 37.486795, + gpsX: 126.947757) + let dummyBus3 = BusPosByRtidDTO(busType: 1, + congestion: 0, + plainNumber: "서울74사5252", + sectionOrder: 4, + fullSectDist: "0.41", + sectDist: "0.022", + gpsY: 37.48311, + gpsX: 126.954122) + return Just([dummyBus1, dummyBus2, dummyBus3]).setFailureType(to: Error.self).eraseToAnyPublisher() + } + } + + override func setUpWithError() throws { + super.setUp() + } + + override func tearDownWithError() throws { + super.tearDown() + } + + func test_bindHeaderInfo_수신_성공() throws { + // given + let viewModel = MovingStatusViewModel(apiUseCase: DummyMovingStatusAPIUseCase(), calculateUseCase: MovingStatusCalculateUseCase(), busRouteId: 100100260, fromArsId: "21211", toArsId: "21210") + let expectation = XCTestExpectation() + let answerHeader = BusInfo(busName: "5524", type: .localLine) + + // when + viewModel.$busInfo + .receive(on: DispatchQueue.global()) + .sink { completion in + // then + guard case .failure(let error) = completion else { return } + XCTFail("\(error.localizedDescription)") + expectation.fulfill() + } receiveValue: { header in + guard let header = header else { return } + // then + XCTAssertEqual(header.busName, answerHeader.busName) + XCTAssertEqual(header.type, answerHeader.type) + expectation.fulfill() + } + .store(in: &self.cancellables) + + wait(for: [expectation], timeout: 10) + } + + func test_bindStationsInfo_수신_성공() throws { + // given + let viewModel = MovingStatusViewModel(apiUseCase: DummyMovingStatusAPIUseCase(), calculateUseCase: MovingStatusCalculateUseCase(), busRouteId: 100100260, fromArsId: "21211", toArsId: "21210") + let expectation = XCTestExpectation() + let station1 = StationInfo(speed: 44, afterSpeed: 29, count: 2, title: "신림복지관앞", sectTime: 0) + let station2 = StationInfo(speed: 29, afterSpeed: nil, count: 2, title: "난우중학교입구", sectTime: Int(ceil(Double(11.4)/Double(21)))) + let answerStations = [station1, station2] + + // when + viewModel.$stationInfos + .receive(on: DispatchQueue.global()) + .filter({ !$0.isEmpty }) + .sink { completion in + // then + guard case .failure(let error) = completion else { return } + XCTFail("\(error.localizedDescription)") + expectation.fulfill() + } receiveValue: { stations in + // then + XCTAssertEqual(stations[0].title, answerStations[0].title) + XCTAssertEqual(stations[0].speed, answerStations[0].speed) + XCTAssertEqual(stations[1].sectTime, answerStations[1].sectTime) + XCTAssertEqual(stations[1].afterSpeed, answerStations[1].afterSpeed) + expectation.fulfill() + } + .store(in: &self.cancellables) + + wait(for: [expectation], timeout: 10) + } + + func test_bindBusesPosInfo_수신_성공() throws { + // given + let viewModel = MovingStatusViewModel(apiUseCase: DummyMovingStatusAPIUseCase(), calculateUseCase: MovingStatusCalculateUseCase(), busRouteId: 100100260, fromArsId: "21211", toArsId: "21210") + let expectation = XCTestExpectation() + let targetBus = BusPosByRtidDTO(busType: 1, + congestion: 0, + plainNumber: "서울74사5254", + sectionOrder: 2, + fullSectDist: "0.378", + sectDist: "0.017", + gpsY: 37.486795, + gpsX: 126.947757) + let answerBuses = [targetBus] + + // when + viewModel.$buses + .receive(on: DispatchQueue.global()) + .filter({ !$0.isEmpty }) + .sink { completion in + // then + guard case .failure(let error) = completion else { return } + XCTFail("\(error.localizedDescription)") + expectation.fulfill() + } receiveValue: { buses in + // then + XCTAssertEqual(buses[0].congestion, answerBuses[0].congestion) + XCTAssertEqual(buses[0].plainNumber, answerBuses[0].plainNumber) + XCTAssertEqual(buses[0].sectionOrder, answerBuses[0].sectionOrder) + XCTAssertEqual(buses[0].gpsX, answerBuses[0].gpsX) + expectation.fulfill() + } + .store(in: &self.cancellables) + + wait(for: [expectation], timeout: 10) + } + + func test_updateRemainingStation() throws { + // given + let viewModel = MovingStatusViewModel(apiUseCase: DummyMovingStatusAPIUseCase(), calculateUseCase: MovingStatusCalculateUseCase(), busRouteId: 100100260, fromArsId: "21211", toArsId: "21210") + let expectation = XCTestExpectation() + let answer: Int? = 1 + + // when + viewModel.$remainingTime + .receive(on: DispatchQueue.global()) + .filter({ $0 != nil }) + .sink { completion in + // then + guard case .failure(let error) = completion else { return } + XCTFail("\(error.localizedDescription)") + expectation.fulfill() + } receiveValue: { remainStation in + // then + XCTAssertEqual(remainStation, answer) + expectation.fulfill() + } + .store(in: &self.cancellables) + + wait(for: [expectation], timeout: 10) + } + + func test_updateBoardBus() throws { +// // given +// let viewModel = MovingStatusViewModel(apiUseCase: DummyMovingStatusAPIUseCase(), calculateUseCase: MovingStatusCalculateUseCase(), busRouteId: 100100260, fromArsId: "21211", toArsId: "21210") +// let expectation = XCTestExpectation() +// let targetBus = BoardedBus(location: CGFloat(Double(("0.017" as NSString).floatValue)/Double(("0.378" as NSString).floatValue)), remainStation: 1) +// +// // when +// viewModel.$boardedBus +// .receive(on: DispatchQueue.global()) +// .filter({ $0 != nil }) +// .sink { completion in +// // then +// guard case .failure(let error) = completion else { return } +// XCTFail("\(error.localizedDescription)") +// expectation.fulfill() +// } receiveValue: { boardedBus in +// guard let boardedBus = boardedBus else { return } +// // then +// XCTAssertEqual(boardedBus.remainStation, targetBus.remainStation) +// XCTAssertEqual(boardedBus.location, targetBus.location) +// expectation.fulfill() +// } +// .store(in: &self.cancellables) +// sleep(1) +// viewModel.findBoardBus(gpsY: 37.486795, gpsX: 126.947757) +// +// wait(for: [expectation], timeout: 10) + } + + func test_remainintTime() throws { + // given + let viewModel = MovingStatusViewModel(apiUseCase: DummyMovingStatusAPIUseCase(), calculateUseCase: MovingStatusCalculateUseCase(), busRouteId: 100100260, fromArsId: "21211", toArsId: "21210") + let expectation = XCTestExpectation() + let answer: Int? = 1 + + // when + viewModel.$remainingTime + .receive(on: DispatchQueue.global()) + .filter({ $0 != nil }) + .sink { completion in + // then + guard case .failure(let error) = completion else { return } + XCTFail("\(error.localizedDescription)") + expectation.fulfill() + } receiveValue: { remainingTime in + // then + XCTAssertEqual(remainingTime, answer) + expectation.fulfill() + } + .store(in: &self.cancellables) + sleep(1) + viewModel.findBoardBus(gpsY: 37.486795, gpsX: 126.947757) + + wait(for: [expectation], timeout: 10) + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/BBus/NetworkServiceTests/NetworkServiceTests.swift b/BBus/NetworkServiceTests/NetworkServiceTests.swift new file mode 100644 index 00000000..91cd2838 --- /dev/null +++ b/BBus/NetworkServiceTests/NetworkServiceTests.swift @@ -0,0 +1,83 @@ +// +// NetworkServiceTests.swift +// NetworkServiceTests +// +// Created by 김태훈 on 2021/11/30. +// + +import XCTest +import Combine + +class NetworkServiceTests: XCTestCase { + + private let timeout: TimeInterval = 10 + private var successRequest: URLRequest? + private var redirectFailRequest: URLRequest? + private var cancellables: Set = [] + + override func setUpWithError() throws { + guard let successRequest = self.makeSuccessRequest(), + let redirectFailRequest = self.makeRedirectRequest() else { throw NetworkError.urlError } + self.successRequest = successRequest + self.redirectFailRequest = redirectFailRequest + } + + private func makeSuccessRequest() -> URLRequest? { + guard let key = Bundle.main.infoDictionary?["API_ACCESS_KEY1"] as? String, + let url = URL(string: "http://ws.bus.go.kr/api/rest/arrive/getLowArrInfoByStId?stId=100&resultType=json&serviceKey=\(key)") + else { return nil } + return URLRequest(url: url) + } + + private func makeRedirectRequest() -> URLRequest? { + guard let url = URL(string: "http://www.naver.com/.png") else { return nil } + return URLRequest(url: url) + } + + func test_get_요청_성공() throws { + // given + let expectation = self.expectation(description: "get 요청이 성공되어야 한다.") + guard let successRequest = self.successRequest else { return } + let networkService = NetworkService() + var data: Data? = nil + + // when + networkService.get(request: successRequest) + .sink(receiveCompletion: { _ in + expectation.fulfill() + }, receiveValue: { result in + data = result + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: self.timeout) + + // then + XCTAssertNotNil(data) + } + + func test_get_리다이렉트로_요청_실패() throws { + // given + let expectation = self.expectation(description: "get 요청이 실패되어야 한다.") + guard let redirectFailRequest = self.redirectFailRequest else { return } + let networkService = NetworkService() + var error: Error? = nil + + // when + networkService.get(request: redirectFailRequest) + .sink(receiveCompletion: { result in + if case .failure(let resultError) = result { + error = resultError + } + expectation.fulfill() + }, receiveValue: { data in + return + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: self.timeout) + + // then + XCTAssertNotNil(error) + } +} diff --git a/BBus/PersistenceStorageTests/PersistenceStorageTests.swift b/BBus/PersistenceStorageTests/PersistenceStorageTests.swift new file mode 100644 index 00000000..9ac40c03 --- /dev/null +++ b/BBus/PersistenceStorageTests/PersistenceStorageTests.swift @@ -0,0 +1,207 @@ +// +// PersistenceStorageTests.swift +// PersistenceStorageTests +// +// Created by 김태훈 on 2021/11/30. +// + +import XCTest +import Combine + +class PersistenceStorageTests: XCTestCase { + + private let key: String = "PersistenceStorageTestKey" + private let key2: String = "PersistenceStorageTestKey2" + private let key3: String = "PersistenceStorageTestKey3" + private let key4: String = "PersistenceStorageTestKey4" + private let fileName: String = "BusRouteList" + private let fileType: String = "json" + private var cancellables: Set = [] + + override func setUpWithError() throws { + self.cancellables = [] + } + + struct DummyCodable: Codable, Equatable { + let dummy: String + } + + func test_create_성공() { + + //given + UserDefaults.standard.removeObject(forKey: self.key) + let storage = PersistenceStorage() + let param = DummyCodable(dummy: "dummy") + let expectation = self.expectation(description: "create가 에러 없이 정상 작동해야함") + var resultOpt: Data? + + //when + storage.create(key: self.key, param: param) + .catchError { error in + XCTFail() + } + .sink { data in + resultOpt = data + expectation.fulfill() + } + .store(in: &self.cancellables) + + //then + waitForExpectations(timeout: 2) + guard let result = resultOpt else { XCTFail(); return } + let decodedResultOpt = try? PropertyListDecoder().decode(DummyCodable.self, from: result) + guard let decodedResult = decodedResultOpt else { XCTFail(); return } + XCTAssertEqual(decodedResult, param, "생성하려는 객체가 정상적으로 리턴되지 않았습니다.") + } + + func test_getFromUserDefault_성공() { + + //given + UserDefaults.standard.removeObject(forKey: self.key2) + + let storage = PersistenceStorage() + let param = DummyCodable(dummy: "dummy") + let createExpect = self.expectation(description: "목 데이터 생성이 선행되어야 함") + let expectation = self.expectation(description: "getFromUserDefault가 에러 없이 정상 작동해야함") + var resultOpt: Data? + + storage.create(key: self.key2, param: param) + .catchError { error in + XCTFail() + } + .sink { _ in + createExpect.fulfill() + } + .store(in: &self.cancellables) + + wait(for: [createExpect], timeout: 5) + //when + storage.getFromUserDefaults(key: self.key2) + .catchError { error in + XCTFail() + } + .sink { data in + resultOpt = data + expectation.fulfill() + } + .store(in: &self.cancellables) + + //then + wait(for: [expectation], timeout: 5) + guard let result = resultOpt else { XCTFail(); return } + let decodedResultOpt = try? PropertyListDecoder().decode([DummyCodable].self, from: result) + guard let decodedResult = decodedResultOpt else { XCTFail(); return } + XCTAssertEqual(decodedResult, [param], "저장된 객체를 불러오지 못했습니다.") + } + + func test_getFromUserDefault_실패() { + + //given + UserDefaults.standard.removeObject(forKey: self.key3) + let storage = PersistenceStorage() + let expectation = self.expectation(description: "데이터가 없기 때문에, getFromUserDefault가 작동하지않음") + var resultOpt: Data? + + //when + storage.getFromUserDefaults(key: self.key3) + .catchError { error in + XCTFail() + } + .sink { data in + resultOpt = data + expectation.fulfill() + } + .store(in: &self.cancellables) + + let emptyArrayDataOpt = try? PropertyListEncoder().encode([FavoriteItemDTO]()) + + //then + waitForExpectations(timeout: 5) + guard let result = resultOpt, + let emptyArrayData = emptyArrayDataOpt else { XCTFail(); return } + XCTAssertEqual(result, emptyArrayData, "저장된 데이터가 존재하면 안됩니다.") + } + + func test_실제저장소_get_BusRouteList_수신성공() { + + //given + let storage = PersistenceStorage() + let expectation = self.expectation(description: "PersistenceStorageRealGet") + + //when + storage.get(file: self.fileName, type: self.fileType) + .catchError { error in + XCTFail() + } + .sink { _ in + expectation.fulfill() + } + .store(in: &self.cancellables) + + //then + waitForExpectations(timeout: 2) + } + + func test_실제저장소_get_BusRouteList_수신실패() { + + //given + let storage = PersistenceStorage() + let expectation = self.expectation(description: "PersistenceStorageRealGet2") + + //when + storage.get(file: "아무파일이름입니다.", type: self.fileType) + .catchError { error in + expectation.fulfill() + } + .sink { data in + XCTFail() + } + .store(in: &self.cancellables) + + //then + waitForExpectations(timeout: 2) + } + + + func test_실제저장소_delete_성공() { + //given + UserDefaults.standard.removeObject(forKey: self.key4) + let storage = PersistenceStorage() + let expectation = self.expectation(description: "데이터를 delete 성공해야함") + var resultOpt: Data? + var params = [DummyCodable(dummy: "dummy1"), DummyCodable(dummy: "dummy2"), DummyCodable(dummy: "dummy3"), DummyCodable(dummy: "dummy4")] + + for param in params { + let createExpect = self.expectation(description: "delete를 위한 목 데이터 생성이 완료되어야함") + storage.create(key: self.key4, param: param) + .catchError { error in + XCTFail() + } + .sink { _ in + // 동시접근 이슈 + createExpect.fulfill() + } + .store(in: &self.cancellables) + wait(for: [createExpect], timeout: 5) + } + + //when + storage.delete(key: self.key4, param: params[2]) + .catchError { error in + XCTFail() + } + .sink { data in + resultOpt = data + expectation.fulfill() + } + .store(in: &self.cancellables) + params.remove(at: 2) // 배열에서도 제거, 비교하기 위함임 + + //then + wait(for: [expectation], timeout: 5) + guard let result = resultOpt else { XCTFail(); return } + let decodedResultOpt = try? PropertyListDecoder().decode([DummyCodable].self, from: result) + guard let decodedResult = decodedResultOpt else { XCTFail(); return } + XCTAssertEqual(params, decodedResult, "제거된 데이터와 상이합니다.") + } +} diff --git a/BBus/RequestFactoryTests/RequestFactoryTests.swift b/BBus/RequestFactoryTests/RequestFactoryTests.swift new file mode 100644 index 00000000..927949ed --- /dev/null +++ b/BBus/RequestFactoryTests/RequestFactoryTests.swift @@ -0,0 +1,67 @@ +// +// RequestFactoryTests.swift +// RequestFactoryTests +// +// Created by 김태훈 on 2021/11/30. +// + +import XCTest + +class RequestFactoryTests: XCTestCase { + + private var requestFactory: Requestable? + + override func setUpWithError() throws { + super.setUp() + self.requestFactory = RequestFactory() + } + + override func tearDownWithError() throws { + super.tearDown() + self.requestFactory = nil + } + + func test_request_파라미터2개_생성_일치() throws { + // given + let mockUrl = "http://ws.bus.go.kr/testUrl" + let mockAccessKey = "uAtMsUVNMLIM%2FM9%3D%3D" + let mockParam = ["stId": "10001", "busRouteId": "1001001"] + let answer1 = URL(string: "http://ws.bus.go.kr/testUrl?stId=10001&busRouteId=1001001&serviceKey=uAtMsUVNMLIM%2FM9%3D%3D") + let answer2 = URL(string: "http://ws.bus.go.kr/testUrl?busRouteId=1001001&stId=10001&serviceKey=uAtMsUVNMLIM%2FM9%3D%3D") + let answers = [answer1, answer2] + + // when + guard let requestResult = self.requestFactory?.request(url: mockUrl, accessKey: mockAccessKey, params: mockParam) else { + XCTFail("request result is nil") + return + } + + // then + XCTAssertNotNil(requestResult) + XCTAssertTrue(answers.contains(requestResult.url)) + } + + func test_request_파라미터3개_생성_일치() throws { + //given + let mockUrl = "http://www.BBus.test" + let mockAccessKey = "uAtMsUVNMLIM%2FM9%3D%3D" + let mockParam = ["stId": "10001", "routeId": "1001001", "ord": "1"] + let answer1 = URL(string: "http://www.BBus.test?stId=10001&routeId=1001001&ord=1&serviceKey=uAtMsUVNMLIM%2FM9%3D%3D") + let answer2 = URL(string: "http://www.BBus.test?routeId=1001001&stId=10001&ord=1&serviceKey=uAtMsUVNMLIM%2FM9%3D%3D") + let answer3 = URL(string: "http://www.BBus.test?ord=1&routeId=1001001&stId=10001&serviceKey=uAtMsUVNMLIM%2FM9%3D%3D") + let answer4 = URL(string: "http://www.BBus.test?stId=10001&ord=1&routeId=1001001&serviceKey=uAtMsUVNMLIM%2FM9%3D%3D") + let answer5 = URL(string: "http://www.BBus.test?routeId=1001001&ord=1&stId=10001&serviceKey=uAtMsUVNMLIM%2FM9%3D%3D") + let answer6 = URL(string: "http://www.BBus.test?ord=1&stId=10001&routeId=1001001&serviceKey=uAtMsUVNMLIM%2FM9%3D%3D") + let answers = [answer1, answer2, answer3, answer4, answer5, answer6] + + // when + guard let requestResult = self.requestFactory?.request(url: mockUrl, accessKey: mockAccessKey, params: mockParam) else { + XCTFail("request result is nil") + return + } + + // then + XCTAssertNotNil(requestResult) + XCTAssertTrue(answers.contains(requestResult.url)) + } +} diff --git a/BBus/SearchViewModelTests/SearchViewModelTests.swift b/BBus/SearchViewModelTests/SearchViewModelTests.swift new file mode 100644 index 00000000..ff5e352b --- /dev/null +++ b/BBus/SearchViewModelTests/SearchViewModelTests.swift @@ -0,0 +1,211 @@ +// +// SearchViewModelTests.swift +// SearchViewModelTests +// +// Created by 김태훈 on 2021/11/30. +// + +import XCTest +import Combine + +class SearchViewModelTests: XCTestCase { + enum MOCKMode { + case success, loadBusRouteFailure, loadStationFailure, alwaysFailure + } + + enum TestError: Error { + case loadBusRouteError, loadStationListError + } + + class MOCKSearchAPIUseCase: SearchAPIUsable { + var mode: MOCKMode + + init(mode: MOCKMode) { + self.mode = mode + } + + func loadBusRouteList() -> AnyPublisher<[BusRouteDTO], Error> { + switch mode { + case .loadBusRouteFailure, .alwaysFailure: + return Fail(error: TestError.loadBusRouteError).eraseToAnyPublisher() + default: + let busRouteDTOs = [ BusRouteDTO(routeID: 1, busRouteName: "1", routeType: .mainLine, startStation: "1s", endStation: "1e"), + BusRouteDTO(routeID: 2, busRouteName: "2", routeType: .mainLine, startStation: "2s", endStation: "2e"), + BusRouteDTO(routeID: 3, busRouteName: "3", routeType: .mainLine, startStation: "3s", endStation: "3e")] + return Just(busRouteDTOs) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + } + + func loadStationList() -> AnyPublisher<[StationDTO], Error> { + switch mode { + case .loadStationFailure, .alwaysFailure: + return Fail(error: TestError.loadStationListError).eraseToAnyPublisher() + default: + let stationDTOs = [StationDTO(stationID: 1, arsID: "1", stationName: "station1"), + StationDTO(stationID: 2, arsID: "2", stationName: "station2"), + StationDTO(stationID: 3, arsID: "3", stationName: "station3"), + StationDTO(stationID: 4, arsID: "4", stationName: "station4")] + return Just(stationDTOs) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + } + } + + private var cancellables: Set! + private let firstBusRoute = BusSearchResult(busRouteDTO: BusRouteDTO(routeID: 1, + busRouteName: "1", + routeType: .mainLine, + startStation: "1s", + endStation: "1e")) + private let firstStation = StationSearchResult(stationName: "station1", + arsId: "1", + stationNameMatchRanges: "station1".ranges(of: "1"), + arsIdMatchRanges: "1".ranges(of: "1")) + + override func setUpWithError() throws { + self.cancellables = [] + super.setUp() + } + + override func tearDownWithError() throws { + self.cancellables = nil + super.tearDown() + } + + func test_bindSearchResult_성공() { + let searchViewModel = SearchViewModel(apiUseCase: MOCKSearchAPIUseCase(mode: .success), + calculateUseCase: SearchCalculateUseCase()) + let keyword = "1" + let expectation = self.expectation(description: "SearchViewModel에 searchResults가 저장되는지 확인") + let expectedResult = SearchResults(busSearchResults: [self.firstBusRoute], stationSearchResults: [self.firstStation]) + let firstExpectedBus = expectedResult.busSearchResults[0] + let firstExpectedStation = expectedResult.stationSearchResults[0] + + // when then + searchViewModel.$searchResults + .receive(on: DispatchQueue.global()) + .dropFirst() + .sink { searchResults in + let firstBus = searchResults.busSearchResults[0] + let firstStation = searchResults.stationSearchResults[0] + + XCTAssertEqual(searchResults.busSearchResults.count, expectedResult.busSearchResults.count) + XCTAssertEqual(firstBus.busRouteName, firstExpectedBus.busRouteName) + XCTAssertEqual(firstBus.routeID, firstExpectedBus.routeID) + XCTAssertEqual(firstBus.routeType, firstExpectedBus.routeType) + XCTAssertEqual(searchResults.stationSearchResults.count, expectedResult.stationSearchResults.count) + XCTAssertEqual(firstStation.arsId, firstExpectedStation.arsId) + XCTAssertEqual(firstStation.arsIdMatchRanges, firstExpectedStation.arsIdMatchRanges) + XCTAssertEqual(firstStation.stationName, firstExpectedStation.stationName) + XCTAssertEqual(firstStation.stationNameMatchRanges, firstExpectedStation.stationNameMatchRanges) + + expectation.fulfill() + } + .store(in: &self.cancellables) + searchViewModel.configure(keyword: keyword) + + waitForExpectations(timeout: 10) + } + + func test_bindSearchResult_버스와_정류소목록_둘_다_없어서_실패() { + let searchViewModel = SearchViewModel(apiUseCase: MOCKSearchAPIUseCase(mode: .alwaysFailure), + calculateUseCase: SearchCalculateUseCase()) + let keyword = "1" + let expectation = self.expectation(description: "SearchViewModel에 searchResults가 저장되는지 확인") + + // when then + searchViewModel.$searchResults + .receive(on: DispatchQueue.global()) + .dropFirst() + .sink { searchResults in + XCTAssertTrue(searchResults.busSearchResults.isEmpty) + XCTAssertTrue(searchResults.busSearchResults.isEmpty) + + expectation.fulfill() + } + .store(in: &self.cancellables) + searchViewModel.configure(keyword: keyword) + + waitForExpectations(timeout: 10) + } + + func test_bindSearchResult_버스_목록이_없어서_실패() { + let searchViewModel = SearchViewModel(apiUseCase: MOCKSearchAPIUseCase(mode: .loadBusRouteFailure), + calculateUseCase: SearchCalculateUseCase()) + let keyword = "1" + let expectation = self.expectation(description: "SearchViewModel에 searchResults가 저장되는지 확인") + let expectedResult = SearchResults(busSearchResults: [], stationSearchResults: [self.firstStation]) + let firstExpectedStation = expectedResult.stationSearchResults[0] + let expectedError = TestError.loadBusRouteError + + // when then + searchViewModel.$searchResults + .receive(on: DispatchQueue.global()) + .dropFirst() + .sink { searchResults in + let firstStation = searchResults.stationSearchResults[0] + + XCTAssertTrue(searchResults.busSearchResults.isEmpty) + XCTAssertEqual(searchResults.stationSearchResults.count, expectedResult.stationSearchResults.count) + XCTAssertEqual(firstStation.arsId, firstExpectedStation.arsId) + XCTAssertEqual(firstStation.arsIdMatchRanges, firstExpectedStation.arsIdMatchRanges) + XCTAssertEqual(firstStation.stationName, firstExpectedStation.stationName) + XCTAssertEqual(firstStation.stationNameMatchRanges, firstExpectedStation.stationNameMatchRanges) + + guard let error = searchViewModel.networkError as? TestError else { XCTFail(); return } + switch error { + case expectedError: + break + default: + XCTFail() + } + + expectation.fulfill() + } + .store(in: &self.cancellables) + searchViewModel.configure(keyword: keyword) + + waitForExpectations(timeout: 10) + } + + func test_bindSearchResult_정류소_목록이_없어서_실패() { + let searchViewModel = SearchViewModel(apiUseCase: MOCKSearchAPIUseCase(mode: .loadStationFailure), + calculateUseCase: SearchCalculateUseCase()) + let keyword = "1" + let expectation = self.expectation(description: "SearchViewModel에 searchResults가 저장되는지 확인") + let expectedResult = SearchResults(busSearchResults:[self.firstBusRoute], stationSearchResults: []) + let firstExpectedBus = expectedResult.busSearchResults[0] + let expectedError = TestError.loadStationListError + + // when then + searchViewModel.$searchResults + .receive(on: DispatchQueue.global()) + .dropFirst() + .sink { searchResults in + let firstBus = searchResults.busSearchResults[0] + + XCTAssertEqual(searchResults.busSearchResults.count, expectedResult.busSearchResults.count) + XCTAssertEqual(firstBus.busRouteName, firstExpectedBus.busRouteName) + XCTAssertEqual(firstBus.routeID, firstExpectedBus.routeID) + XCTAssertEqual(firstBus.routeType, firstExpectedBus.routeType) + XCTAssertTrue(searchResults.stationSearchResults.isEmpty) + + guard let error = searchViewModel.networkError as? TestError else { XCTFail(); return } + switch error { + case expectedError: + break + default: + XCTFail() + } + + expectation.fulfill() + } + .store(in: &self.cancellables) + searchViewModel.configure(keyword: keyword) + + waitForExpectations(timeout: 10) + } +} diff --git a/BBus/StationViewModelTests/DummyJsonStringStationByUidItemDTO.json b/BBus/StationViewModelTests/DummyJsonStringStationByUidItemDTO.json new file mode 100644 index 00000000..67ada527 --- /dev/null +++ b/BBus/StationViewModelTests/DummyJsonStringStationByUidItemDTO.json @@ -0,0 +1,231 @@ +{ + "comMsgHeader": { + "responseMsgID": null, + "responseTime": null, + "requestMsgID": null, + "returnCode": null, + "errMsg": null, + "successYN": null + }, + "msgHeader": { + "headerMsg": "정상적으로 처리되었습니다.", + "headerCd": "0", + "itemCount": 0 + }, + "msgBody": { + "itemList": [ + { + "stId": "123000624", + "stNm": "위례신안인스빌.중앙푸르지오", + "arsId": "24411", + "busRouteId": "100100496", + "rtNm": "333", + "sectNm": "위례35단지아파트~위례중앙푸르지오", + "gpsX": "127.1438406379", + "gpsY": "37.475507478", + "posX": "212721.6958061627", + "posY": "441801.72879467765", + "stationTp": "0", + "firstTm": "0400 ", + "lastTm": "2300 ", + "term": "12", + "routeType": "3", + "nextBus": " ", + "staOrd": "7", + "vehId1": "0", + "plainNo1": null, + "sectOrd1": "0", + "stationNm1": null, + "traTime1": "0", + "traSpd1": "0", + "isArrive1": "0", + "repTm1": "2021-04-15 12:08:39.0", + "isLast1": "0", + "busType1": "1", + "vehId2": "0", + "plainNo2": null, + "sectOrd2": "0", + "stationNm2": null, + "traTime2": "0", + "traSpd2": "0", + "isArrive2": "0", + "repTm2": null, + "isLast2": "0", + "busType2": "0", + "adirection": "올림픽공원", + "arrmsg1": "2분20초후[1번째 전]", + "arrmsg2": "7분20초후[3번째 전]", + "arrmsgSec1": "첫 번째 버스 운행종료", + "arrmsgSec2": "두 번째 버스 운행종료", + "nxtStn": "송파와이즈더샵.엠코타운센트로엘", + "rerdieDiv1": "2", + "rerdieDiv2": "2", + "rerideNum1": "0", + "rerideNum2": "0", + "isFullFlag1": "0", + "isFullFlag2": "0", + "deTourAt": "00", + "congestion": "0" + }, + { + "stId": "123000624", + "stNm": "위례신안인스빌.중앙푸르지오", + "arsId": "24411", + "busRouteId": "107000002", + "rtNm": "343", + "sectNm": "위례35단지아파트~위례중앙푸르지오", + "gpsX": "127.1438406379", + "gpsY": "37.475507478", + "posX": "212721.6958061627", + "posY": "441801.72879467765", + "stationTp": "0", + "firstTm": "0430 ", + "lastTm": "2300 ", + "term": "15", + "routeType": "4", + "nextBus": " ", + "staOrd": "7", + "vehId1": "0", + "plainNo1": null, + "sectOrd1": "0", + "stationNm1": null, + "traTime1": "0", + "traSpd1": "0", + "isArrive1": "0", + "repTm1": "2021-04-15 12:09:10.0", + "isLast1": "0", + "busType1": "0", + "vehId2": "0", + "plainNo2": null, + "sectOrd2": "0", + "stationNm2": null, + "traTime2": "0", + "traSpd2": "0", + "isArrive2": "0", + "repTm2": null, + "isLast2": "0", + "busType2": "0", + "adirection": "수서역", + "arrmsg1": "운행종료", + "arrmsg2": "운행종료", + "arrmsgSec1": "첫 번째 버스 운행종료", + "arrmsgSec2": "두 번째 버스 운행종료", + "nxtStn": "서울위례별초교", + "rerdieDiv1": "2", + "rerdieDiv2": "2", + "rerideNum1": "0", + "rerideNum2": "0", + "isFullFlag1": "0", + "isFullFlag2": "0", + "deTourAt": "00", + "congestion": "0" + }, + { + "stId": "123000624", + "stNm": "위례신안인스빌.중앙푸르지오", + "arsId": "24411", + "busRouteId": "100100459", + "rtNm": "440", + "sectNm": "위례35단지아파트~위례중앙푸르지오", + "gpsX": "127.1438406379", + "gpsY": "37.475507478", + "posX": "212721.6958061627", + "posY": "441801.72879467765", + "stationTp": "0", + "firstTm": "0355 ", + "lastTm": "2255 ", + "term": "12", + "routeType": "2", + "nextBus": " ", + "staOrd": "7", + "vehId1": "0", + "plainNo1": null, + "sectOrd1": "0", + "stationNm1": null, + "traTime1": "0", + "traSpd1": "0", + "isArrive1": "0", + "repTm1": "2021-04-15 12:08:32.0", + "isLast1": "0", + "busType1": "1", + "vehId2": "0", + "plainNo2": null, + "sectOrd2": "0", + "stationNm2": null, + "traTime2": "0", + "traSpd2": "0", + "isArrive2": "0", + "repTm2": null, + "isLast2": "0", + "busType2": "0", + "adirection": "압구정동한양파출소앞", + "arrmsg1": "운행종료", + "arrmsg2": "운행종료", + "arrmsgSec1": "첫 번째 버스 운행종료", + "arrmsgSec2": "두 번째 버스 운행종료", + "nxtStn": "송파와이즈더샵.엠코타운센트로엘", + "rerdieDiv1": "2", + "rerdieDiv2": "2", + "rerideNum1": "0", + "rerideNum2": "0", + "isFullFlag1": "0", + "isFullFlag2": "0", + "deTourAt": "00", + "congestion": "0" + }, + { + "stId": "123000624", + "stNm": "위례신안인스빌.중앙푸르지오", + "arsId": "24411", + "busRouteId": "227000035", + "rtNm": "9202하남", + "sectNm": "엠코타운플로리체~위례신안인스빌.중앙푸르지오", + "gpsX": "127.1438406379", + "gpsY": "37.475507478", + "posX": "212721.6958061627", + "posY": "441801.72879467765", + "stationTp": "0", + "firstTm": "0430 ", + "lastTm": "2220 ", + "term": "42", + "routeType": "8", + "nextBus": " ", + "staOrd": "90", + "vehId1": "227000164", + "plainNo1": null, + "sectOrd1": "90", + "stationNm1": "위례신안인스빌.중앙푸르지오", + "traTime1": "5", + "traSpd1": "0", + "isArrive1": "0", + "repTm1": null, + "isLast1": "0", + "busType1": "0", + "vehId2": "0", + "plainNo2": null, + "sectOrd2": "0", + "stationNm2": null, + "traTime2": "0", + "traSpd2": "0", + "isArrive2": "0", + "repTm2": null, + "isLast2": "0", + "busType2": "0", + "adirection": "상산곡동.공영차고지", + "arrmsg1": "곧 도착", + "arrmsg2": "운행종료", + "arrmsgSec1": "곧 도착", + "arrmsgSec2": "두 번째 버스 운행종료", + "nxtStn": "송파와이즈더샵.엠코타운센트로엘", + "rerdieDiv1": "2", + "rerdieDiv2": "2", + "rerideNum1": "0", + "rerideNum2": "0", + "isFullFlag1": "0", + "isFullFlag2": "0", + "deTourAt": "00", + "congestion": "0" + } + ] + } +} diff --git a/BBus/StationViewModelTests/StationViewModelTests.swift b/BBus/StationViewModelTests/StationViewModelTests.swift new file mode 100644 index 00000000..81b2a611 --- /dev/null +++ b/BBus/StationViewModelTests/StationViewModelTests.swift @@ -0,0 +1,566 @@ +// +// StationViewModelTests.swift +// StationViewModelTests +// +// Created by 김태훈 on 2021/11/30. +// + +import XCTest +import Combine + +class StationViewModelTests: XCTestCase { + + enum TestError: Error { + case unknownError, urlError, dataError, noneError + + func publisher() -> AnyPublisher { + let publisher = CurrentValueSubject(nil) + publisher.send(completion: .failure(self)) + return publisher.compactMap({$0}) + .eraseToAnyPublisher() + } + } + + enum MOCKType { + case success, failure + } + + struct MOCKStationAPIUseCase: StationAPIUsable { + + private let type: MOCKType + + init(_ type: MOCKType) { + self.type = type + } + + func loadStationList() -> AnyPublisher<[StationDTO], Error> { + let stationDTOs: [StationDTO] + switch self.type { + case .success : + stationDTOs = [StationDTO(stationID: 1, arsID: "1", stationName: "station1"), + StationDTO(stationID: 2, arsID: "2", stationName: "station2"), + StationDTO(stationID: 3, arsID: "3", stationName: "station3"), + StationDTO(stationID: 4, arsID: "4", stationName: "station4")] + case .failure : + stationDTOs = [StationDTO(stationID: 1, arsID: "2", stationName: "station1"), + StationDTO(stationID: 2, arsID: "3", stationName: "station2"), + StationDTO(stationID: 3, arsID: "4", stationName: "station3"), + StationDTO(stationID: 4, arsID: "5", stationName: "station4")] + } + return stationDTOs.publisher + .setFailureType(to: Error.self) + .collect() + .eraseToAnyPublisher() + } + + func refreshInfo(about arsId: String) -> AnyPublisher<[StationByUidItemDTO], Error> { + guard let url = Bundle.init(identifier: "com.boostcamp.ios-009.StationViewModelTests")?.url(forResource: "DummyJsonStringStationByUidItemDTO", withExtension: "json") else { + return TestError.urlError.publisher() + } + if let data = try? Data(contentsOf: url), + let stationByUidDTOs = (try? JSONDecoder().decode(StationByUidItemResult.self, from: data))?.msgBody.itemList { + return stationByUidDTOs.publisher + .setFailureType(to: Error.self) + .collect() + .eraseToAnyPublisher() + } else { + return TestError.dataError.publisher() + } + } + + func add(favoriteItem: FavoriteItemDTO) -> AnyPublisher { + let data = try? PropertyListEncoder().encode(favoriteItem) + return data.publisher + .setFailureType(to: Error.self) + .compactMap({$0}) + .eraseToAnyPublisher() + } + + func remove(favoriteItem: FavoriteItemDTO) -> AnyPublisher { + self.add(favoriteItem: favoriteItem) + } + + func getFavoriteItems() -> AnyPublisher<[FavoriteItemDTO], Error> { + let favoriteItemsDTOs = [FavoriteItemDTO(stId: "1", busRouteId: "1", ord: "1", arsId: "1"), + FavoriteItemDTO(stId: "1", busRouteId: "3", ord: "2", arsId: "4"), + FavoriteItemDTO(stId: "1", busRouteId: "1", ord: "1", arsId: "1"), + FavoriteItemDTO(stId: "3", busRouteId: "2", ord: "2", arsId: "2")] + return favoriteItemsDTOs.publisher + .setFailureType(to: Error.self) + .collect() + .eraseToAnyPublisher() + } + + func loadRoute() -> AnyPublisher<[BusRouteDTO], Error> { + let busRouteDTOs: [BusRouteDTO] + switch self.type { + case .success : + busRouteDTOs = [BusRouteDTO(routeID: 100100496, + busRouteName: "bus1", + routeType: RouteType.broadArea, + startStation: "station1", + endStation: "station5"), + BusRouteDTO(routeID: 100100459, + busRouteName: "bus2", + routeType: RouteType.broadArea, + startStation: "station2", + endStation: "station100"), + BusRouteDTO(routeID: 107000002, + busRouteName: "bus3", + routeType: RouteType.circulation, + startStation: "station1", + endStation: "station5")] + case .failure : + busRouteDTOs = [BusRouteDTO(routeID: 1, + busRouteName: "bus1", + routeType: RouteType.broadArea, + startStation: "station1", + endStation: "station5")] + } + return busRouteDTOs.publisher + .setFailureType(to: Error.self) + .collect() + .eraseToAnyPublisher() + } + } + + private let timeout: TimeInterval = 10 + private var cancellables: Set = [] + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func test_bindStationInfo_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 stationInfo가 정상적으로 왔는지 확인") + let expectationResult = StationDTO(stationID: 1, arsID: "1", stationName: "station1") + + // when + stationViewModel.$stationInfo + .sink(receiveValue: { [weak expectation] _ in + expectation?.fulfill() + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: timeout) + + // then + XCTAssertEqual(stationViewModel.stationInfo?.arsID, expectationResult.arsID) + XCTAssertEqual(stationViewModel.stationInfo?.stationName, expectationResult.stationName) + XCTAssertEqual(stationViewModel.stationInfo?.stationID, expectationResult.stationID) + } + + func test_bindStationInfo_일치하는_Station_정보가_없는_경우() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.failure), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "일치하는 stationInfo가 없는 경우 에러를 반환하는지 확인") + var error: Error? = nil + + // when + stationViewModel.$error + .sink(receiveCompletion: { _ in + return + }, receiveValue: { [weak expectation] result in + error = result + expectation?.fulfill() + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: timeout) + + // then + XCTAssertNotNil(error) + } + + func test_bindFavoriteItems_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 favoriteItems가 정상적으로 왔는지 확인") + let expectationResult = [FavoriteItemDTO(stId: "1", busRouteId: "1", ord: "1", arsId: "1"), + FavoriteItemDTO(stId: "1", busRouteId: "1", ord: "1", arsId: "1")] + + // when + stationViewModel.$favoriteItems + .compactMap({$0}) + .sink(receiveCompletion: { _ in + return + }, receiveValue: { [weak expectation] _ in + expectation?.fulfill() + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: timeout) + + // then + XCTAssertEqual(stationViewModel.favoriteItems?.count ?? 0, expectationResult.count) + stationViewModel.favoriteItems?.enumerated().forEach({ index, favoriteItem in + XCTAssertEqual(favoriteItem.arsId, expectationResult[index].arsId) + XCTAssertEqual(favoriteItem.busRouteId, expectationResult[index].busRouteId) + XCTAssertEqual(favoriteItem.ord, expectationResult[index].ord) + XCTAssertEqual(favoriteItem.stId, expectationResult[index].stId) + }) + } + + func test_bindFavoriteItems_빈_배열_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "0") + let expectation = self.expectation(description: "StationViewModel에 favoriteItems가 빈배열로 정상적으로 왔는지 확인") + + // when + stationViewModel.$favoriteItems + .compactMap({$0}) + .sink(receiveValue: { [weak expectation] _ in + expectation?.fulfill() + }) + .store(in: &self.cancellables) + + waitForExpectations(timeout: self.timeout) + + // then + XCTAssertNotNil(stationViewModel.favoriteItems) + XCTAssertEqual(stationViewModel.favoriteItems?.count ?? 0, 0) + } + + func test_refresh_nextStation_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 nextStation가 정상적으로 왔는지 확인") + let expectationResult = "송파와이즈더샵.엠코타운센트로엘" + var nextStation: String? = nil + + // when + stationViewModel.$nextStation + .compactMap({$0}) + .sink(receiveValue: { [weak expectation] result in + nextStation = result + expectation?.fulfill() + }) + .store(in: &self.cancellables) + stationViewModel.refresh() + + waitForExpectations(timeout: timeout) + + // then + XCTAssertEqual(nextStation, expectationResult) + } + + func test_refresh_nextStation_nil_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.failure), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 nextStation이 nil로 오는지 확인") + var nextStation: String? = nil + + stationViewModel.$nextStation + .output(at: 1) + .sink(receiveValue: { [weak expectation] result in + nextStation = result + expectation?.fulfill() + }) + .store(in: &self.cancellables) + stationViewModel.refresh() + + waitForExpectations(timeout: timeout) + + // then + XCTAssertNil(nextStation) + } + + + func test_refresh_activeBuses_정상_할당_확인() { + // given + let stationViewModel = StationViewModel(apiUseCase: MOCKStationAPIUseCase(.success), + calculateUseCase: StationCalculateUseCase(), + arsId: "1") + let expectation = self.expectation(description: "StationViewModel에 activeBuses가 정상적으로 왔는지 확인") + let busArriveInfo: BusArriveInfo + busArriveInfo.nextStation = "송파와이즈더샵.엠코타운센트로엘" + busArriveInfo.busRouteId = 100100496 + busArriveInfo.busNumber = "333" + busArriveInfo.routeType = BBusRouteType.gansun + busArriveInfo.stationOrd = 7 + busArriveInfo.firstBusArriveRemainTime = BusRemainTime(arriveRemainTime: "2분20초") + busArriveInfo.firstBusRelativePosition = "1번째전" + busArriveInfo.firstBusCongestion = BusCongestion(rawValue: 0) + busArriveInfo.secondBusArriveRemainTime = BusRemainTime(arriveRemainTime: "7분20초") + busArriveInfo.secondBusRelativePosition = "3번째전" + busArriveInfo.secondBusCongestion = BusCongestion(rawValue: 0) + + let expectationResult = [BBusRouteType.gansun: BusArriveInfos(infos: [busArriveInfo])] + + // when + stationViewModel.$busKeys + .dropFirst() + .sink(receiveValue: { [weak expectation] a in + expectation?.fulfill() + }) + .store(in: &self.cancellables) + stationViewModel.refresh() + + waitForExpectations(timeout: self.timeout) + + // then + XCTAssertEqual(stationViewModel.activeBuses.count, expectationResult.count) + stationViewModel.activeBuses.forEach({ key, info in + guard info.count() == expectationResult[key]?.count() ?? 0 else { return XCTFail() } + for i in 0.. [String] { + var accessKeyList = [String]() + + for i in (1...TokenManager.maxTokenCount) { + guard let accessKey = Bundle.main.infoDictionary?["API_ACCESS_KEY\(i)"] as? String else { throw APIAccessKeyError.cannotFindAccessKey } + accessKeyList.append(accessKey) + } + + return accessKeyList + } + + func test_randomAccessKey_리턴_성공() { + // given + let expectedMinIndex = 0 + let expectedMaxIndex = 16 + + // when then + do { + let randomAccessKey = try self.tokenManager.randomAccessKey() + let index = randomAccessKey.index + let key = randomAccessKey.key + + XCTAssertGreaterThanOrEqual(index, expectedMinIndex) + XCTAssertLessThanOrEqual(index, expectedMaxIndex) + XCTAssertTrue(self.accessKeyList.contains(key)) + } catch { + XCTFail() + } + } + + func test_randomAccessKey_액세스키_없어_리턴_실패() { + // given + for i in (0..