From 8d6571a77f3f44add3b8b942f7b7dbddad5f28c3 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 11 Feb 2026 11:11:14 +0900 Subject: [PATCH 01/17] =?UTF-8?q?[FEAT]=20=EC=A7=80=ED=95=98=EC=B2=A0=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=A1=B0=ED=9A=8C=20UseCase/Repo?= =?UTF-8?q?sitory=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS.xcodeproj/project.pbxproj | 48 +++++++++++++++++++ .../SubwayRealTimeInfoRequest.swift | 12 +++++ .../SubwayRealTimeInfoResponse.swift | 45 +++++++++++++++++ .../Repository/SubwayInfoRepositoryImpl.swift | 31 ++++++++++++ .../SubwayInfo/SubwayRealTimeInfo.swift | 33 +++++++++++++ .../Repository/SubwayInfoRepository.swift | 12 +++++ .../Domain/UseCase/SubwayInfoUseCase.swift | 27 +++++++++++ 7 files changed, 208 insertions(+) create mode 100644 Atcha-iOS/Data/Model/SubwayInfoDTO/SubwayRealTimeInfo/SubwayRealTimeInfoRequest.swift create mode 100644 Atcha-iOS/Data/Model/SubwayInfoDTO/SubwayRealTimeInfo/SubwayRealTimeInfoResponse.swift create mode 100644 Atcha-iOS/Data/Repository/SubwayInfoRepositoryImpl.swift create mode 100644 Atcha-iOS/Domain/Entity/SubwayInfo/SubwayRealTimeInfo.swift create mode 100644 Atcha-iOS/Domain/Repository/SubwayInfoRepository.swift create mode 100644 Atcha-iOS/Domain/UseCase/SubwayInfoUseCase.swift diff --git a/Atcha-iOS.xcodeproj/project.pbxproj b/Atcha-iOS.xcodeproj/project.pbxproj index f0c1a48..c3510b1 100644 --- a/Atcha-iOS.xcodeproj/project.pbxproj +++ b/Atcha-iOS.xcodeproj/project.pbxproj @@ -20,6 +20,12 @@ 6D1EE2F72E0A933000F7BBF1 /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D1EE2F62E0A933000F7BBF1 /* OnboardingCoordinator.swift */; }; 6D26DFA82F289E64005097A4 /* HeadingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D26DFA72F289E64005097A4 /* HeadingManager.swift */; }; 6D26DFAA2F2D3B27005097A4 /* CloseOnlyNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D26DFA92F2D3B27005097A4 /* CloseOnlyNavigationBar.swift */; }; + 6D26DFFC2F3C16FD005097A4 /* SubwayInfoUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D26DFFB2F3C16FD005097A4 /* SubwayInfoUseCase.swift */; }; + 6D26E0002F3C17C3005097A4 /* SubwayRealTimeInfoRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D26DFFF2F3C17C3005097A4 /* SubwayRealTimeInfoRequest.swift */; }; + 6D26E0022F3C17DE005097A4 /* SubwayRealTimeInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D26E0012F3C17DE005097A4 /* SubwayRealTimeInfoResponse.swift */; }; + 6D26E0052F3C18B0005097A4 /* SubwayRealTimeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D26E0042F3C18B0005097A4 /* SubwayRealTimeInfo.swift */; }; + 6D26E0072F3C197F005097A4 /* SubwayInfoRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D26E0062F3C197F005097A4 /* SubwayInfoRepository.swift */; }; + 6D26E0092F3C1A0C005097A4 /* SubwayInfoRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D26E0082F3C1A0C005097A4 /* SubwayInfoRepositoryImpl.swift */; }; 6D2B8C942E38A97500608104 /* BusDetailHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2B8C932E38A97500608104 /* BusDetailHeaderView.swift */; }; 6D2B8C972E38ABF600608104 /* BusInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2B8C962E38ABF600608104 /* BusInfoViewController.swift */; }; 6D2B8C992E38ABFD00608104 /* BusInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2B8C982E38ABFD00608104 /* BusInfoViewModel.swift */; }; @@ -336,6 +342,12 @@ 6D1EE2F62E0A933000F7BBF1 /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = ""; }; 6D26DFA72F289E64005097A4 /* HeadingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadingManager.swift; sourceTree = ""; }; 6D26DFA92F2D3B27005097A4 /* CloseOnlyNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseOnlyNavigationBar.swift; sourceTree = ""; }; + 6D26DFFB2F3C16FD005097A4 /* SubwayInfoUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubwayInfoUseCase.swift; sourceTree = ""; }; + 6D26DFFF2F3C17C3005097A4 /* SubwayRealTimeInfoRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubwayRealTimeInfoRequest.swift; sourceTree = ""; }; + 6D26E0012F3C17DE005097A4 /* SubwayRealTimeInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubwayRealTimeInfoResponse.swift; sourceTree = ""; }; + 6D26E0042F3C18B0005097A4 /* SubwayRealTimeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubwayRealTimeInfo.swift; sourceTree = ""; }; + 6D26E0062F3C197F005097A4 /* SubwayInfoRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubwayInfoRepository.swift; sourceTree = ""; }; + 6D26E0082F3C1A0C005097A4 /* SubwayInfoRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubwayInfoRepositoryImpl.swift; sourceTree = ""; }; 6D2B8C932E38A97500608104 /* BusDetailHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusDetailHeaderView.swift; sourceTree = ""; }; 6D2B8C962E38ABF600608104 /* BusInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusInfoViewController.swift; sourceTree = ""; }; 6D2B8C982E38ABFD00608104 /* BusInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusInfoViewModel.swift; sourceTree = ""; }; @@ -703,6 +715,31 @@ path = HeadingManager; sourceTree = ""; }; + 6D26DFFD2F3C179D005097A4 /* SubwayInfoDTO */ = { + isa = PBXGroup; + children = ( + 6D26DFFE2F3C17B3005097A4 /* SubwayRealTimeInfo */, + ); + path = SubwayInfoDTO; + sourceTree = ""; + }; + 6D26DFFE2F3C17B3005097A4 /* SubwayRealTimeInfo */ = { + isa = PBXGroup; + children = ( + 6D26DFFF2F3C17C3005097A4 /* SubwayRealTimeInfoRequest.swift */, + 6D26E0012F3C17DE005097A4 /* SubwayRealTimeInfoResponse.swift */, + ); + path = SubwayRealTimeInfo; + sourceTree = ""; + }; + 6D26E0032F3C189D005097A4 /* SubwayInfo */ = { + isa = PBXGroup; + children = ( + 6D26E0042F3C18B0005097A4 /* SubwayRealTimeInfo.swift */, + ); + path = SubwayInfo; + sourceTree = ""; + }; 6D2B8C952E38ABED00608104 /* BusInfo */ = { isa = PBXGroup; children = ( @@ -1128,6 +1165,7 @@ B65C12CF2E04292F0016D2F0 /* Entity */ = { isa = PBXGroup; children = ( + 6D26E0032F3C189D005097A4 /* SubwayInfo */, 6D2B8CA02E39A73300608104 /* BusInfo */, B65C12DD2E042CF00016D2F0 /* UserInfo.swift */, B66401842E2244CF00A397AE /* Location.swift */, @@ -1147,6 +1185,7 @@ 6DC3BF5F2E071F0900831470 /* LoginUseCase.swift */, 6D1EE2ED2E0A7D7300F7BBF1 /* OnboardingUseCase.swift */, 6D73EBE52E1B51E000F8DF8B /* CourseUseCase.swift */, + 6D26DFFB2F3C16FD005097A4 /* SubwayInfoUseCase.swift */, 6D2B8CB92E39C8E100608104 /* BusInfoUseCase.swift */, 6DB7636A2E45C5EC00D06A49 /* AlarmUseCase.swift */, ); @@ -1162,6 +1201,7 @@ 6D1EE2EF2E0A7F0000F7BBF1 /* OnboardingRepository.swift */, 6D73EBE72E1B54A500F8DF8B /* CourseRepository.swift */, 6D2B8CBD2E39C9CB00608104 /* BusInfoRepository.swift */, + 6D26E0062F3C197F005097A4 /* SubwayInfoRepository.swift */, 6DB7636C2E45C69400D06A49 /* AlarmRepository.swift */, ); path = Repository; @@ -1181,6 +1221,7 @@ children = ( B6C5074B2E129656000AB39F /* Location */, 6D2B8CBF2E39CC8800608104 /* BusInfoRepositoryImpl.swift */, + 6D26E0082F3C1A0C005097A4 /* SubwayInfoRepositoryImpl.swift */, B65C12E32E042D6B0016D2F0 /* UserRepositoryImpl.swift */, B65C131F2E058C360016D2F0 /* AppVersionRepositoryImpl.swift */, 6D73EBE92E1B551600F8DF8B /* CourseRepositoryImpl.swift */, @@ -1265,6 +1306,7 @@ 6DB763672E45C52600D06A49 /* AlarmDTO */, 6D6879CC2E42119500E59C55 /* UserInfoPatchDTO */, 6D2B8C9F2E39A70500608104 /* BusInfoDTO */, + 6D26DFFD2F3C179D005097A4 /* SubwayInfoDTO */, B6AC8BDC2E2BAD6100410ECD /* UserDTO */, 6D5E03BA2E24F3D50065AFBE /* SearchHistoryDTO */, B664017E2E2243C800A397AE /* LocationDTO */, @@ -1988,6 +2030,7 @@ 6D91A90F2E38A1C60081BAFC /* BusDetailViewModel.swift in Sources */, B6CF46F52E09620100304A85 /* LoginCoordinator.swift in Sources */, B6FB208A2E1BE5200032751B /* FetchTaxiFareRepositoryImpl.swift in Sources */, + 6D26DFFC2F3C16FD005097A4 /* SubwayInfoUseCase.swift in Sources */, 6DB4DE932F114D6A00F2DC4E /* AtchaActionToast.swift in Sources */, B68309532E005A3600E2D029 /* AppDelegate.swift in Sources */, B65402D52E76CACC00AB5862 /* DetailRouteSummaryView.swift in Sources */, @@ -2042,6 +2085,7 @@ B6793D542E386DFE001BE9F5 /* DetailRouteViewController.swift in Sources */, B65C12FE2E057AF90016D2F0 /* NetworkConstant.swift in Sources */, B6AC8BE72E2BC4E300410ECD /* AlarmSettingDIContainer.swift in Sources */, + 6D26E0092F3C1A0C005097A4 /* SubwayInfoRepositoryImpl.swift in Sources */, 6D8D8B132E01B338003DABB2 /* AtchaButton.swift in Sources */, B66401832E22449000A397AE /* SearchLocationResponse.swift in Sources */, 6D368BA42E02D5E200144EE7 /* IconTitleNavigationBar.swift in Sources */, @@ -2084,6 +2128,7 @@ 6D26DFA82F289E64005097A4 /* HeadingManager.swift in Sources */, 6D91A9012E33E3620081BAFC /* LoadingView.swift in Sources */, B664018B2E2277A900A397AE /* PushAlarmOption.swift in Sources */, + 6D26E0072F3C197F005097A4 /* SubwayInfoRepository.swift in Sources */, B65C13082E057C590016D2F0 /* APIService.swift in Sources */, 6D6879D02E4211B800E59C55 /* HomePatchRequest.swift in Sources */, 6D9283742E3AFF6A0090889B /* BusRouteCell.swift in Sources */, @@ -2165,10 +2210,12 @@ 6DD632C22E544DB500C6A66E /* PushAlarmBottomView.swift in Sources */, 6D5E03D02E28853E0065AFBE /* CourseSearchResponse.swift in Sources */, 6D91A8E62E29F5BC0081BAFC /* CourseSettingViewModel.swift in Sources */, + 6D26E0002F3C17C3005097A4 /* SubwayRealTimeInfoRequest.swift in Sources */, B673C4912E0424FD00EE4AD0 /* SplashViewModel.swift in Sources */, 6DB763692E45C53A00D06A49 /* AlarmRequest.swift in Sources */, B6C507502E129693000AB39F /* LocationStreamRepositoryImpl.swift in Sources */, B6AC8BD92E2A85D800410ECD /* SignOutUseCase.swift in Sources */, + 6D26E0022F3C17DE005097A4 /* SubwayRealTimeInfoResponse.swift in Sources */, 6D1EE2F02E0A7F0000F7BBF1 /* OnboardingRepository.swift in Sources */, 6D1EE2E52E09A42F00F7BBF1 /* PushAlarmViewController.swift in Sources */, B6793D4F2E34972B001BE9F5 /* FetchTaxiFareRequest.swift in Sources */, @@ -2223,6 +2270,7 @@ B69E31AC2E2683B8001040F4 /* PermissionViewController.swift in Sources */, 6D1EE2EE2E0A7D7300F7BBF1 /* OnboardingUseCase.swift in Sources */, B6AC8BDB2E2B3AA200410ECD /* LogoutuseCase.swift in Sources */, + 6D26E0052F3C18B0005097A4 /* SubwayRealTimeInfo.swift in Sources */, 6DFF29912E0CBE820039399F /* RegisterLocationViewController.swift in Sources */, B6793D692E3B3FB8001BE9F5 /* DetailRouteSubwayCell.swift in Sources */, B6793D602E3A5E23001BE9F5 /* DetailRouteProgressView.swift in Sources */, diff --git a/Atcha-iOS/Data/Model/SubwayInfoDTO/SubwayRealTimeInfo/SubwayRealTimeInfoRequest.swift b/Atcha-iOS/Data/Model/SubwayInfoDTO/SubwayRealTimeInfo/SubwayRealTimeInfoRequest.swift new file mode 100644 index 0000000..502842e --- /dev/null +++ b/Atcha-iOS/Data/Model/SubwayInfoDTO/SubwayRealTimeInfo/SubwayRealTimeInfoRequest.swift @@ -0,0 +1,12 @@ +// +// BusRealTimeInfoRequest.swift +// Atcha-iOS +// +// Created by wodnd on 2/11/26. +// + +import Foundation + +struct SubwayRealTimeInfoRequest: Codable { + let routeName: String +} diff --git a/Atcha-iOS/Data/Model/SubwayInfoDTO/SubwayRealTimeInfo/SubwayRealTimeInfoResponse.swift b/Atcha-iOS/Data/Model/SubwayInfoDTO/SubwayRealTimeInfo/SubwayRealTimeInfoResponse.swift new file mode 100644 index 0000000..b360f14 --- /dev/null +++ b/Atcha-iOS/Data/Model/SubwayInfoDTO/SubwayRealTimeInfo/SubwayRealTimeInfoResponse.swift @@ -0,0 +1,45 @@ +// +// BusRealTimeInfoResponse.swift +// Atcha-iOS +// +// Created by wodnd on 2/11/26. +// + +import Foundation + +struct SubwayRealTimeInfoResponse: Codable { + let routeName: String? + let subwayArrivalStatus: String? + let remainingTime: Int? + let isLast: Bool? + let expectedArrivalTime: String? + let trainNo: String? + let destination: String? + let direction: String? +} + +extension SubwayRealTimeInfoResponse { + func toEntity() -> SubwayRealTimeInfo? { + guard + let routeName, + let subwayArrivalStatus, + let remainingTime, + let isLast, + let expectedArrivalTime, + let trainNo, + let destination, + let direction + else { return nil } + + return SubwayRealTimeInfo( + routeName: routeName, + subwayArrivalStatus: subwayArrivalStatus, + remainingTime: remainingTime, + isLast: isLast, + expectedArrivalTime: expectedArrivalTime, + trainNo: trainNo, + destination: destination, + direction: direction + ) + } +} diff --git a/Atcha-iOS/Data/Repository/SubwayInfoRepositoryImpl.swift b/Atcha-iOS/Data/Repository/SubwayInfoRepositoryImpl.swift new file mode 100644 index 0000000..3457de2 --- /dev/null +++ b/Atcha-iOS/Data/Repository/SubwayInfoRepositoryImpl.swift @@ -0,0 +1,31 @@ +// +// SubwayInfoRepositoryImpl.swift +// Atcha-iOS +// +// Created by wodnd on 2/11/26. +// + +import Foundation +import Alamofire + +final class SubwayInfoRepositoryImpl: SubwayInfoRepository { + + private let apiService: APIService + + init(apiService: APIService) { + self.apiService = apiService + } + + // 실시간 지하철 정보 조회 + func subwayRealTimeInfo(_ request: SubwayRealTimeInfoRequest) async throws -> [SubwayRealTimeInfoResponse] { + let res: APIResponse<[SubwayRealTimeInfoResponse]> = try await apiService.request( + Endpoint( + path: "/routes/user-routes/subway-arrival", + method: .post, + encoding: JSONEncoding.default + ), + body: request + ) + return res.result ?? [] + } +} diff --git a/Atcha-iOS/Domain/Entity/SubwayInfo/SubwayRealTimeInfo.swift b/Atcha-iOS/Domain/Entity/SubwayInfo/SubwayRealTimeInfo.swift new file mode 100644 index 0000000..0379906 --- /dev/null +++ b/Atcha-iOS/Domain/Entity/SubwayInfo/SubwayRealTimeInfo.swift @@ -0,0 +1,33 @@ +// +// SubwayRealTimeInfo.swift +// Atcha-iOS +// +// Created by wodnd on 2/11/26. +// + +import Foundation + +enum SubwayStatus: String, Decodable { + case approaching = "APPROACHING" + case arriving = "ARRIVING" + case departed = "DEPARTED" + case operating = "OPERATING" + case waiting = "WAITING" + case unknown + + init(from decoder: Decoder) throws { + let raw = try decoder.singleValueContainer().decode(String.self) + self = SubwayStatus(rawValue: raw) ?? .unknown + } +} + +struct SubwayRealTimeInfo: Codable { + let routeName: String? + let subwayArrivalStatus: String? + let remainingTime: Int? + let isLast: Bool? + let expectedArrivalTime: String? + let trainNo: String? + let destination: String? + let direction: String? +} diff --git a/Atcha-iOS/Domain/Repository/SubwayInfoRepository.swift b/Atcha-iOS/Domain/Repository/SubwayInfoRepository.swift new file mode 100644 index 0000000..b407c3c --- /dev/null +++ b/Atcha-iOS/Domain/Repository/SubwayInfoRepository.swift @@ -0,0 +1,12 @@ +// +// SubwayInfoRepository.swift +// Atcha-iOS +// +// Created by wodnd on 2/11/26. +// + +import Foundation +protocol SubwayInfoRepository { + // 실시간 지하철 정보 조회 + func subwayRealTimeInfo(_ request: SubwayRealTimeInfoRequest) async throws -> [SubwayRealTimeInfoResponse] +} diff --git a/Atcha-iOS/Domain/UseCase/SubwayInfoUseCase.swift b/Atcha-iOS/Domain/UseCase/SubwayInfoUseCase.swift new file mode 100644 index 0000000..2b2a084 --- /dev/null +++ b/Atcha-iOS/Domain/UseCase/SubwayInfoUseCase.swift @@ -0,0 +1,27 @@ +// +// SubwayInfoUseCase.swift +// Atcha-iOS +// +// Created by wodnd on 2/11/26. +// + +import Foundation + +protocol SubwayInfoUseCase { + // 실시간 지하철 정보 조회 + func subwayRealTimeInfo(_ request: SubwayRealTimeInfoRequest) async throws -> [SubwayRealTimeInfo] +} + +final class SubwayInfoUseCaseImpl: SubwayInfoUseCase { + private let repository: SubwayInfoRepository + + init(repository: SubwayInfoRepository) { + self.repository = repository + } + + // 실시간 버스 정보 조회 + func subwayRealTimeInfo(_ request: SubwayRealTimeInfoRequest) async throws -> [SubwayRealTimeInfo] { + let dtos = try await repository.subwayRealTimeInfo(request) + return dtos.compactMap { $0.toEntity() } + } +} From 558f451041c8645ee95ea9e53117f0ed93dde546 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 11 Feb 2026 12:16:39 +0900 Subject: [PATCH 02/17] =?UTF-8?q?[FEAT]=20=EC=A7=80=ED=95=98=EC=B2=A0=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=ED=98=B8=EC=B6=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DIContainer/Route/RouteDIContainer.swift | 2 + .../Repository/SubwayInfoRepositoryImpl.swift | 16 ++++---- .../DetailRoute/DetailRouteViewModel.swift | 41 ++++++++++++++++--- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/Atcha-iOS/App/DIContainer/Route/RouteDIContainer.swift b/Atcha-iOS/App/DIContainer/Route/RouteDIContainer.swift index 952a286..e37da4f 100644 --- a/Atcha-iOS/App/DIContainer/Route/RouteDIContainer.swift +++ b/Atcha-iOS/App/DIContainer/Route/RouteDIContainer.swift @@ -13,6 +13,7 @@ final class RouteDIContainer { private lazy var requestUseCase = RequestLocationAuthorizationUseCaseImpl(repository: PermissionRepositoryImpl()) private lazy var streamUseCase = ObserLocationStreamUseCaseImpl(repository: LocationStreamRepositoryImpl()) private lazy var busInfoUseCase = BusInfoUseCaseImpl(repository: BusInfoRepositoryImpl(apiService: apiService)) + private lazy var subwayInfoUseCase = SubwayInfoUseCaseImpl(repository: SubwayInfoRepositoryImpl(apiService: apiService)) private lazy var alarmUseCase = AlarmUseCaseImpl(repository: AlarmRepositoryImpl(apiService: apiService)) init(apiService: APIService) { @@ -24,6 +25,7 @@ final class RouteDIContainer { infos: infos, context: context, busInfoUseCase: busInfoUseCase, + subwayInfoUseCase: subwayInfoUseCase, authorizationUseCase: requestUseCase, streamUseCase: streamUseCase, alarmUseCase: alarmUseCase) diff --git a/Atcha-iOS/Data/Repository/SubwayInfoRepositoryImpl.swift b/Atcha-iOS/Data/Repository/SubwayInfoRepositoryImpl.swift index 3457de2..0204fe0 100644 --- a/Atcha-iOS/Data/Repository/SubwayInfoRepositoryImpl.swift +++ b/Atcha-iOS/Data/Repository/SubwayInfoRepositoryImpl.swift @@ -18,14 +18,14 @@ final class SubwayInfoRepositoryImpl: SubwayInfoRepository { // 실시간 지하철 정보 조회 func subwayRealTimeInfo(_ request: SubwayRealTimeInfoRequest) async throws -> [SubwayRealTimeInfoResponse] { - let res: APIResponse<[SubwayRealTimeInfoResponse]> = try await apiService.request( - Endpoint( - path: "/routes/user-routes/subway-arrival", - method: .post, - encoding: JSONEncoding.default - ), - body: request + let endpoint = Endpoint( + path: "/routes/user-routes/subway-arrival", + method: .get, + parameters: ["routeName": request.routeName], + encoding: URLEncoding.queryString ) - return res.result ?? [] + + let result: [SubwayRealTimeInfoResponse] = try await apiService.request(endpoint) + return result } } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift index d196d35..3754068 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift @@ -16,6 +16,7 @@ enum DetailRouteContext { final class DetailRouteViewModel: BaseViewModel { private let busInfoUseCase: BusInfoUseCase + private let subwayInfoUseCase: SubwayInfoUseCase private let authorizationUseCase: RequestLocationAuthorizationUseCase private let streamUseCase: ObserveLocationStreamUseCase private let alarmUseCase: AlarmUseCase @@ -33,6 +34,7 @@ final class DetailRouteViewModel: BaseViewModel { // @Published var busRealTimeInfo: [RealTimeBusArrival] = [] @Published var busRealTimeInfos: [[RealTimeBusArrival]] = [] + @Published var subwayRealTimeInfos: [SubwayRealTimeInfo] = [] @Published private(set) var context: DetailRouteContext @@ -40,6 +42,7 @@ final class DetailRouteViewModel: BaseViewModel { infos: LegInfo, context: DetailRouteContext, busInfoUseCase: BusInfoUseCase, + subwayInfoUseCase: SubwayInfoUseCase, authorizationUseCase: RequestLocationAuthorizationUseCase, streamUseCase: ObserveLocationStreamUseCase, alarmUseCase: AlarmUseCase) { @@ -47,6 +50,7 @@ final class DetailRouteViewModel: BaseViewModel { self.address = address self.context = context self.busInfoUseCase = busInfoUseCase + self.subwayInfoUseCase = subwayInfoUseCase self.authorizationUseCase = authorizationUseCase self.streamUseCase = streamUseCase self.alarmUseCase = alarmUseCase @@ -57,18 +61,31 @@ final class DetailRouteViewModel: BaseViewModel { } func fetchInfo() { - self.legtPathInfo = infos.pathInfo - self.legTrafficInfo = infos.trafficInfo + legtPathInfo = infos.pathInfo + legTrafficInfo = infos.trafficInfo + + // 버스 let busDetailInfo = infos.busInfo.filter { $0.routeName?.isEmpty == false } - busRealTimeInfos = [] busDetailInfo.forEach { info in if let routeName = info.routeName, routeName.contains(":") { - Task { - await getBusRealTimeInfo(request: routeName) - } + Task { await getBusRealTimeInfo(request: routeName) } } } + + guard context == .afterReigster else { return } + + subwayRealTimeInfos = [] + let subwayRoutes = Array(Set( + infos.trafficInfo + .filter { $0.mode == .subway } + .compactMap { $0.route } + .filter { !$0.isEmpty } + )) + + subwayRoutes.forEach { route in + Task { await getSubwayRealTimeInfo(routeName: route) } + } } @MainActor @@ -85,6 +102,18 @@ final class DetailRouteViewModel: BaseViewModel { } } + @MainActor + func getSubwayRealTimeInfo(routeName: String) async { + do { + let infos = try await subwayInfoUseCase.subwayRealTimeInfo(.init(routeName: routeName)) + subwayRealTimeInfos.removeAll { $0.routeName == routeName } // 기존 제거 + subwayRealTimeInfos.append(contentsOf: infos) + print("지하철 정보:\(infos)") + } catch { + print("실시간 지하철 조회 실패: \(error)") + } + } + func requestPermissionAndStartTracking() { Task { let status = await authorizationUseCase.askLocationPermission() From 83762aa927bce6a82c671d6394352df7564eea69 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 11 Feb 2026 12:25:58 +0900 Subject: [PATCH 03/17] =?UTF-8?q?[REFACTOR]=20=EA=B2=BD=EB=A1=9C=ED=83=90?= =?UTF-8?q?=EC=83=89=20=EC=A7=80=ED=95=98=EC=B2=A0=EC=A0=95=EB=A5=98?= =?UTF-8?q?=EC=9E=A5=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CourseSearch/CourseStepItemView.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Atcha-iOS/Presentation/Course/CourseSearch/CourseStepItemView.swift b/Atcha-iOS/Presentation/Course/CourseSearch/CourseStepItemView.swift index 17facd4..92ca942 100644 --- a/Atcha-iOS/Presentation/Course/CourseSearch/CourseStepItemView.swift +++ b/Atcha-iOS/Presentation/Course/CourseSearch/CourseStepItemView.swift @@ -72,8 +72,6 @@ final class CourseStepItemView: UIView { isHidden = false - stationLabel.attributedText = AtchaFont.R_12(leg.start?.name ?? "", color: AtchaColor.white) - busNumberLabel.isHidden = true endStationLabel.isHidden = true endPointView.isHidden = true @@ -85,6 +83,8 @@ final class CourseStepItemView: UIView { switch leg.mode { case .bus: + stationLabel.attributedText = AtchaFont.R_12(leg.start?.name ?? "", color: AtchaColor.white) + let key = leg.type ?? "0" let imageName = busIcon[key] ?? busDefaultIcon trafficImageView.image = UIImage(named: imageName) @@ -138,13 +138,18 @@ final class CourseStepItemView: UIView { } } case .subway: + let startText = stationText(leg.start?.name) + stationLabel.attributedText = AtchaFont.R_12(startText, color: AtchaColor.white) + let key = leg.type ?? "0" let imageName = subwayIcon[key] ?? subwayDefaultIcon trafficImageView.image = UIImage(named: imageName) if isLast { endStationLabel.isHidden = false - endStationLabel.attributedText = AtchaFont.R_12(leg.end?.name ?? "", color: AtchaColor.white) + + let endText = stationText(leg.end?.name) + endStationLabel.attributedText = AtchaFont.R_12(endText, color: AtchaColor.white) endStationLabel.snp.remakeConstraints { make in make.leading.equalTo(stationLabel.snp.leading) @@ -179,6 +184,7 @@ final class CourseStepItemView: UIView { } default: trafficImageView.image = nil + stationLabel.attributedText = AtchaFont.R_12(leg.start?.name ?? "", color: AtchaColor.white) stationLabel.snp.remakeConstraints { make in make.leading.equalTo(trafficImageView.snp.trailing).offset(8) make.top.equalTo(trafficImageView.snp.top) @@ -187,4 +193,10 @@ final class CourseStepItemView: UIView { } } } + + private func stationText(_ name: String?) -> String { + let n = (name ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + guard !n.isEmpty else { return "" } + return n.hasSuffix("역") ? n : "\(n)역" + } } From 63b1c013b639fc4a65201e9ebb8b297895a3417c Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 11 Feb 2026 13:27:07 +0900 Subject: [PATCH 04/17] =?UTF-8?q?[REFACTOR]=20=EC=83=81=EC=84=B8=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cell/DetailRouteBusCell.swift | 17 ++++++++--------- .../Cell/DetailRouteSubwayCell.swift | 8 ++++---- .../DetailRouteInfoBottomView.swift | 2 +- .../DetailRouteSummaryView.swift | 5 +++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift index cc5aed7..b5c8869 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift @@ -421,17 +421,16 @@ extension DetailRouteBusCell { @objc private func handleSummaryButton() { isExpanded.toggle() stationListStackView.isHidden = !isExpanded - + stationListStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } addStationNameLabel(info: stationInfos) - - UIView.animate(withDuration: 0.3) { [weak self] in - guard let self else { return } - stationListStackViewTopConstraint?.isActive = isExpanded - stationListStackViewBottomConstraint?.isActive = isExpanded - endLabelTopConstraintWithoutStack?.isActive = !isExpanded - self.layoutIfNeeded() - } + + stationListStackViewTopConstraint?.isActive = isExpanded + stationListStackViewBottomConstraint?.isActive = isExpanded + endLabelTopConstraintWithoutStack?.isActive = !isExpanded + + self.layoutIfNeeded() + didTapSummary?() } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift index 3fa68b6..2cc643f 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift @@ -300,15 +300,15 @@ extension DetailRouteSubwayCell { @objc private func handleSummaryButton() { isExpanded.toggle() stationListStackView.isHidden = !isExpanded - + stationListStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } addStationNameLabel(info: stationInfos) - + stationListStackViewTopConstraint?.isActive = isExpanded stationListStackViewBottomConstraint?.isActive = isExpanded endLabelTopConstraintWithoutStack?.isActive = !isExpanded - - UIView.animate(withDuration: 0.3) { self.layoutIfNeeded() } + layoutIfNeeded() + didTapSummary?() } } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift index 53ebcd7..a228a0e 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift @@ -141,7 +141,7 @@ final class DetailRouteInfoBottomView: UIView { collectionView.reloadData() } - private func applySnapshot(animatingDifferences: Bool = true) { + private func applySnapshot(animatingDifferences: Bool = false) { guard collectionView.dataSource != nil else { return } dataSource.apply(snapshot, animatingDifferences: animatingDifferences) } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteSummaryView.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteSummaryView.swift index 019114d..c6ba43e 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteSummaryView.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteSummaryView.swift @@ -42,14 +42,15 @@ final class DetailRouteSummaryView: UIView { } arrowImageView.snp.makeConstraints { make in - make.centerY.equalTo(summaryLabel) make.leading.equalTo(summaryLabel.snp.trailing).offset(4) make.trailing.equalToSuperview() - make.width.height.equalTo(10) + make.top.bottom.equalToSuperview() + make.width.equalTo(10) } } func configure(duration: String, stops: Int) { summaryLabel.attributedText = AtchaFont.B7_M_13("\(duration), \(stops - 1)개 정류장 이동", color: .white) + arrowImageView.isHidden = false } } From a0b156d41ae5bef76ae6219cfa90d7e8e640b846 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 11 Feb 2026 14:28:44 +0900 Subject: [PATCH 05/17] =?UTF-8?q?[REFACTOR]=20=EC=83=81=EC=84=B8=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20AutoLayout=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cell/DetailRouteBusCell.swift | 77 +++++++++++++++---- .../Cell/DetailRouteSubwayCell.swift | 72 ++++++++++++++--- .../DetailRouteInfoBottomView.swift | 10 ++- 3 files changed, 130 insertions(+), 29 deletions(-) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift index b5c8869..dc44030 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift @@ -93,6 +93,7 @@ final class DetailRouteBusCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } + // MARK: - 수정 1: prepareForReuse에서 constraint 상태 리셋 추가 override func prepareForReuse() { super.prepareForReuse() @@ -100,6 +101,34 @@ final class DetailRouteBusCell: UICollectionViewCell { animationView.stopAnimation() backgroundColor = .clear busTimerStackView.isHidden = true + + isExpanded = false + stationListStackView.isHidden = true + stationListStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + stationListStackViewTopConstraint?.isActive = false + stationListStackViewBottomConstraint?.isActive = false + endLabelTopConstraintWithoutStack?.isActive = true + } + + override func preferredLayoutAttributesFitting( + _ layoutAttributes: UICollectionViewLayoutAttributes + ) -> UICollectionViewLayoutAttributes { + setNeedsLayout() + layoutIfNeeded() + + let targetSize = CGSize( + width: layoutAttributes.frame.width, + height: UIView.layoutFittingCompressedSize.height + ) + let size = contentView.systemLayoutSizeFitting( + targetSize, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel + ) + var newAttributes = layoutAttributes + newAttributes.frame.size.height = ceil(size.height) + return newAttributes } private func setupUI() { @@ -134,7 +163,7 @@ final class DetailRouteBusCell: UICollectionViewCell { private func setupInitialConstraintState() { stationListStackViewTopConstraint?.isActive = false stationListStackViewBottomConstraint?.isActive = false - endLabelTopConstraintWithoutStack?.isActive = true // ✅ isExpanded = false 상태 + endLabelTopConstraintWithoutStack?.isActive = true } private func setupLineImageView() { @@ -177,6 +206,8 @@ final class DetailRouteBusCell: UICollectionViewCell { } } + // DetailRouteBusCell.swift + private func setupInfoConstrains() { busBadgeView.snp.makeConstraints { make in make.leading.equalTo(startLabel.snp.leading) @@ -195,8 +226,15 @@ final class DetailRouteBusCell: UICollectionViewCell { stationListStackView.snp.makeConstraints { $0.leading.equalTo(startLabel) - stationListStackViewTopConstraint = $0.top.equalTo(summaryView.snp.bottom).offset(16).constraint - stationListStackViewBottomConstraint = $0.bottom.equalTo(endLabel.snp.top).offset(-28).constraint + stationListStackViewTopConstraint = $0.top + .equalTo(summaryView.snp.bottom) + .offset(16) + .constraint + stationListStackViewBottomConstraint = $0.bottom + .equalTo(endStackView.snp.top) // ← endLabel 대신 endStackView 기준으로 변경 + .offset(-28) + .priority(.high) + .constraint } } @@ -208,9 +246,13 @@ final class DetailRouteBusCell: UICollectionViewCell { make.edges.equalToSuperview().inset(10) } endStackView.snp.makeConstraints { make in - endLabelTopConstraintWithoutStack = make.top.equalTo(summaryView.snp.bottom).offset(36).constraint + endLabelTopConstraintWithoutStack = make.top + .equalTo(summaryView.snp.bottom) + .offset(36) + .priority(.high) + .constraint make.horizontalEdges.equalToSuperview().offset(16) - make.bottom.equalToSuperview() + make.bottom.equalToSuperview().priority(.high) make.height.equalTo(36) } } @@ -420,18 +462,25 @@ extension DetailRouteBusCell { @objc private func handleSummaryButton() { isExpanded.toggle() + + if isExpanded { + endLabelTopConstraintWithoutStack?.isActive = false + stationListStackViewTopConstraint?.isActive = true + stationListStackViewBottomConstraint?.isActive = true + } else { + stationListStackViewTopConstraint?.isActive = false + stationListStackViewBottomConstraint?.isActive = false + endLabelTopConstraintWithoutStack?.isActive = true + } + stationListStackView.isHidden = !isExpanded - stationListStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } addStationNameLabel(info: stationInfos) - - stationListStackViewTopConstraint?.isActive = isExpanded - stationListStackViewBottomConstraint?.isActive = isExpanded - endLabelTopConstraintWithoutStack?.isActive = !isExpanded - - self.layoutIfNeeded() - - didTapSummary?() + + self.contentView.setNeedsLayout() + self.contentView.layoutIfNeeded() + + didTapSummary?() // ← 이 안에서 applySnapshot() 호출됨 } @objc private func handleBusBackTapped() { diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift index 2cc643f..d883226 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift @@ -75,6 +75,34 @@ final class DetailRouteSubwayCell: UICollectionViewCell { animationView.isHidden = true animationView.stopAnimation() backgroundColor = .clear + + isExpanded = false + stationListStackView.isHidden = true + stationListStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + stationListStackViewTopConstraint?.isActive = false + stationListStackViewBottomConstraint?.isActive = false + endLabelTopConstraintWithoutStack?.isActive = true + } + + override func preferredLayoutAttributesFitting( + _ layoutAttributes: UICollectionViewLayoutAttributes + ) -> UICollectionViewLayoutAttributes { + setNeedsLayout() + layoutIfNeeded() + + let targetSize = CGSize( + width: layoutAttributes.frame.width, + height: UIView.layoutFittingCompressedSize.height + ) + let size = contentView.systemLayoutSizeFitting( + targetSize, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel + ) + var newAttributes = layoutAttributes + newAttributes.frame.size.height = ceil(size.height) + return newAttributes } private func setupUI() { @@ -169,8 +197,16 @@ final class DetailRouteSubwayCell: UICollectionViewCell { stationListStackView.snp.makeConstraints { $0.leading.equalTo(startLabel) - stationListStackViewTopConstraint = $0.top.equalTo(summaryView.snp.bottom).offset(16).constraint - stationListStackViewBottomConstraint = $0.bottom.equalTo(endLabel.snp.top).offset(-28).constraint + stationListStackViewTopConstraint = $0.top + .equalTo(summaryView.snp.bottom) + .offset(16) + .constraint + + stationListStackViewBottomConstraint = $0.bottom + .equalTo(endStackView.snp.top) + .offset(-28) + .priority(.high) + .constraint } } @@ -182,11 +218,17 @@ final class DetailRouteSubwayCell: UICollectionViewCell { make.edges.equalToSuperview().inset(10) } endStackView.snp.makeConstraints { make in - endLabelTopConstraintWithoutStack = make.top.equalTo(summaryView.snp.bottom).offset(36).constraint + endLabelTopConstraintWithoutStack = make.top + .equalTo(summaryView.snp.bottom) + .offset(36) + .priority(.high) // ✅ high 우선순위 + .constraint make.horizontalEdges.equalToSuperview().offset(16) - make.bottom.equalToSuperview() + make.bottom.equalToSuperview().priority(.high) // ✅ high 우선순위 make.height.equalTo(36) } + + endLabelTopConstraintWithoutStack?.isActive = false } private func setupConstraints() { @@ -299,16 +341,24 @@ extension DetailRouteSubwayCell { @objc private func handleSummaryButton() { isExpanded.toggle() + + if isExpanded { + endLabelTopConstraintWithoutStack?.isActive = false + stationListStackViewTopConstraint?.isActive = true + stationListStackViewBottomConstraint?.isActive = true + } else { + stationListStackViewTopConstraint?.isActive = false + stationListStackViewBottomConstraint?.isActive = false + endLabelTopConstraintWithoutStack?.isActive = true + } + stationListStackView.isHidden = !isExpanded - stationListStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } addStationNameLabel(info: stationInfos) - - stationListStackViewTopConstraint?.isActive = isExpanded - stationListStackViewBottomConstraint?.isActive = isExpanded - endLabelTopConstraintWithoutStack?.isActive = !isExpanded - layoutIfNeeded() - + + contentView.setNeedsLayout() + contentView.layoutIfNeeded() + didTapSummary?() } } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift index a228a0e..18ebce1 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift @@ -202,8 +202,8 @@ extension DetailRouteInfoBottomView { case .transport(let transportMode): switch transportMode { case .walk: height = 74 - case .bus: height = 175 - case .subway: height = 154 + case .bus: height = 200 + case .subway: height = 200 case .unknown: height = 38 } case .end: height = 58 @@ -250,7 +250,8 @@ extension DetailRouteInfoBottomView { for: indexPath ) as! DetailRouteBusCell cell.didTapSummary = { [weak self] in - self?.applySnapshot() +// self?.applySnapshot() + self?.collectionView.collectionViewLayout.invalidateLayout() } cell.getNewBusRealTime = { [weak self] in self?.getNewBusRealTime?() @@ -295,7 +296,8 @@ extension DetailRouteInfoBottomView { for: indexPath ) as! DetailRouteSubwayCell cell.didTapSummary = { [weak self] in - self?.applySnapshot() +// self?.applySnapshot() + self?.collectionView.collectionViewLayout.invalidateLayout() } cell.configure(info: item.info) return cell From 7017524b8c85dcd64f3b912459ffe8ec3b523ad1 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 11 Feb 2026 14:31:45 +0900 Subject: [PATCH 06/17] =?UTF-8?q?[REFACTOR]=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=9D=8C=EB=9F=89=2030=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS/Core/Manager/AlarmManager.swift | 2 +- .../Setting/PushAlarm/PushAlarmBottomView.swift | 6 +++--- .../Setting/PushAlarm/PushAlarmViewController.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Atcha-iOS/Core/Manager/AlarmManager.swift b/Atcha-iOS/Core/Manager/AlarmManager.swift index 832ef46..9196d8a 100644 --- a/Atcha-iOS/Core/Manager/AlarmManager.swift +++ b/Atcha-iOS/Core/Manager/AlarmManager.swift @@ -415,7 +415,7 @@ extension AlarmManager { // 1) UserDefaults에서 값 다시 읽기 let savedVolume = UserDefaultsWrapper.shared.float( forKey: UserDefaultsWrapper.Key.alarmVolume.rawValue - ) ?? 0.7 + ) ?? 0.3 // 2) AlarmManager 상태 업데이트 alarmVolume = savedVolume diff --git a/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmBottomView.swift b/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmBottomView.swift index f2df6cf..0cb4331 100644 --- a/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmBottomView.swift +++ b/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmBottomView.swift @@ -13,7 +13,7 @@ import MediaPlayer final class PushAlarmBottomView: UIView { private var currentVolume: Float = AVAudioSession.sharedInstance().outputVolume private var volumeObservation: NSKeyValueObservation? - private var volume: Float = 0.7 + private var volume: Float = 0.3 private let volumeTitleLabel: UILabel = UILabel() private let volumeSubTitleLabel: UILabel = UILabel() @@ -61,11 +61,11 @@ final class PushAlarmBottomView: UIView { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(sliderTapped(_:))) volumeSlider.addGestureRecognizer(tapGesture) - volumeSlider.setValue(0.7, animated: true) + volumeSlider.setValue(0.3, animated: true) let savedVolume = UserDefaultsWrapper.shared.float( forKey: UserDefaultsWrapper.Key.alarmVolume.rawValue - ) ?? 0.7 + ) ?? 0.3 volumeSlider.setValue(savedVolume, animated: false) diff --git a/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmViewController.swift b/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmViewController.swift index a2b3349..f4fdba5 100644 --- a/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmViewController.swift +++ b/Atcha-iOS/Presentation/Setting/PushAlarm/PushAlarmViewController.swift @@ -218,11 +218,11 @@ final class PushAlarmViewController: BaseViewController { case .onlySound: self.toggleBottomView(show: true) - AlarmManager.shared.previewAlarmVolume(0.7) + AlarmManager.shared.previewAlarmVolume(0.3) case .both: self.toggleBottomView(show: true) - AlarmManager.shared.previewAlarmVolume(0.7) + AlarmManager.shared.previewAlarmVolume(0.3) } } From 98dbff3cd9db1975bab383335783b0b35c9c8ad6 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Wed, 11 Feb 2026 15:09:26 +0900 Subject: [PATCH 07/17] =?UTF-8?q?[FEAT]=20=EC=9D=BC=EB=B0=98=EB=B2=84?= =?UTF-8?q?=EC=8A=A4=20=EC=83=89=EC=83=81=20=EB=B0=8F=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSource/AtchaColor/AtchaColor.swift | 1 + .../Bus/general.colorset/Contents.json | 20 +++++++++++++++ .../Icon.xcassets/Color/General/Contents.json | 6 +++++ .../general-getOff.imageset/Contents.json | 23 ++++++++++++++++++ .../general-getOff.png | Bin 0 -> 445 bytes .../general-getOff@2x.png | Bin 0 -> 789 bytes .../general-getOff@3x.png | Bin 0 -> 1074 bytes .../General/general.imageset/Contents.json | 23 ++++++++++++++++++ .../General/general.imageset/general.png | Bin 0 -> 606 bytes .../General/general.imageset/general@2x.png | Bin 0 -> 1034 bytes .../General/general.imageset/general@3x.png | Bin 0 -> 1516 bytes .../general-border.imageset/Contents.json | 12 +++++++++ .../general-border.svg | 4 +++ .../Bus/bus-general.imageset/Contents.json | 23 ++++++++++++++++++ .../Bus/bus-general.imageset/bus-general.png | Bin 0 -> 326 bytes .../bus-general.imageset/bus-general@2x.png | Bin 0 -> 423 bytes .../bus-general.imageset/bus-general@3x.png | Bin 0 -> 603 bytes .../Course/Bus20/General/Contents.json | 6 +++++ .../bus-general-20px.imageset/Contents.json | 23 ++++++++++++++++++ .../bus-general-20px.png | Bin 0 -> 572 bytes .../bus-general-20px@2x.png | Bin 0 -> 995 bytes .../bus-general-20px@3x.png | Bin 0 -> 1350 bytes .../Course/BusRoute/General/Contents.json | 6 +++++ .../Contents.json | 23 ++++++++++++++++++ .../general-long-opacity.png | Bin 0 -> 601 bytes .../general-long-opacity@2x.png | Bin 0 -> 1135 bytes .../general-long-opacity@3x.png | Bin 0 -> 1835 bytes .../general-long-turn.imageset/Contents.json | 23 ++++++++++++++++++ .../general-long-turn.png | Bin 0 -> 982 bytes .../general-long-turn@2x.png | Bin 0 -> 1988 bytes .../general-long-turn@3x.png | Bin 0 -> 2976 bytes .../general-long.imageset/Contents.json | 23 ++++++++++++++++++ .../general-long.imageset/general-long.png | Bin 0 -> 591 bytes .../general-long.imageset/general-long@2x.png | Bin 0 -> 1131 bytes .../general-long.imageset/general-long@3x.png | Bin 0 -> 1837 bytes .../Contents.json | 23 ++++++++++++++++++ .../general-short-opacity.png | Bin 0 -> 589 bytes .../general-short-opacity@2x.png | Bin 0 -> 1085 bytes .../general-short-opacity@3x.png | Bin 0 -> 1687 bytes .../general-short-turn.imageset/Contents.json | 23 ++++++++++++++++++ .../general-short-turn.png | Bin 0 -> 927 bytes .../general-short-turn@2x.png | Bin 0 -> 1792 bytes .../general-short-turn@3x.png | Bin 0 -> 2654 bytes .../general-short.imageset/Contents.json | 23 ++++++++++++++++++ .../general-short.imageset/general-short.png | Bin 0 -> 580 bytes .../general-short@2x.png | Bin 0 -> 1081 bytes .../general-short@3x.png | Bin 0 -> 1686 bytes .../AtchaImage/TransportationIconView.swift | 16 ++++++------ .../Presentation/BusDetail/BusRouteCell.swift | 12 ++++++++- 49 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 Atcha-iOS/DesignSource/AtchaColor/Colors.xcassets/Transportation/Bus/general.colorset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general-getOff.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general-getOff.imageset/general-getOff.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general-getOff.imageset/general-getOff@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general-getOff.imageset/general-getOff@3x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general.imageset/general.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general.imageset/general@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general.imageset/general@3x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/border/general-border.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/border/general-border.imageset/general-border.svg create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/bus-general.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/bus-general@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/bus-general@3x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/bus-general-20px.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/bus-general-20px@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/bus-general-20px@3x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/general-long-opacity.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/general-long-opacity@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/general-long-opacity@3x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-turn.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-turn.imageset/general-long-turn.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-turn.imageset/general-long-turn@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-turn.imageset/general-long-turn@3x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long.imageset/general-long.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long.imageset/general-long@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long.imageset/general-long@3x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/general-short-opacity.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/general-short-opacity@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/general-short-opacity@3x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/general-short-turn.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/general-short-turn@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/general-short-turn@3x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/Contents.json create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/general-short.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/general-short@2x.png create mode 100644 Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/general-short@3x.png diff --git a/Atcha-iOS/DesignSource/AtchaColor/AtchaColor.swift b/Atcha-iOS/DesignSource/AtchaColor/AtchaColor.swift index 273276e..c81ac04 100644 --- a/Atcha-iOS/DesignSource/AtchaColor/AtchaColor.swift +++ b/Atcha-iOS/DesignSource/AtchaColor/AtchaColor.swift @@ -44,6 +44,7 @@ enum AtchaColor{ static let town = UIColor(named: "town")! static let mainline = UIColor(named: "mainline")! static let widearea = UIColor(named: "widearea")! + static let general = UIColor(named: "general")! } // MARK: - Transportation/Subway diff --git a/Atcha-iOS/DesignSource/AtchaColor/Colors.xcassets/Transportation/Bus/general.colorset/Contents.json b/Atcha-iOS/DesignSource/AtchaColor/Colors.xcassets/Transportation/Bus/general.colorset/Contents.json new file mode 100644 index 0000000..c95002f --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaColor/Colors.xcassets/Transportation/Bus/general.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA9", + "green" : "0x9B", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general-getOff.imageset/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general-getOff.imageset/Contents.json new file mode 100644 index 0000000..f00c72e --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general-getOff.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "general-getOff.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "general-getOff@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "general-getOff@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general-getOff.imageset/general-getOff.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general-getOff.imageset/general-getOff.png new file mode 100644 index 0000000000000000000000000000000000000000..e6d495874a07e3feded1509c610216f28ef2142e GIT binary patch literal 445 zcmV;u0Yd(XP)~rT=3C$=%^6 zP4CYC0%f>=eM?M~?$Q!O&tfIritc{qQqo;mE0TWw( z0_jn2uShm1&pf|{n6(s{jG@RQyK)ILxn$$F)RB>b!W=pYffU9@p466A1a4%@ssYh4 z6cO_Ws_u>beQn8nGcu?9B!+rVBiY*n`&d`-&i%1Lzn_?Xy9{q@$Fwz0?q#nH@SQY2J7SponVg^ zvdm!xNiEc*PUqg`i{!#H2#Zrk4&#tz;nnIFoj8tb*B2#3DXEj%N4r8h$p6QCg^Zov85n8y--(Y8Sl6 zSXYUwQcH6mbzMCuZG(x>5$mpRAn~H0uczFU$41UpqST+(9l#qCFY}=+WgR0~KvCwX z^}|fAX_x2Rx=YokkF2d)>}_sSaIMjOf1)e+3L`4k@nfwY?)t;c8nCyujnwNmXdjez zRw+?k)e`?Ub!1h#gHA)J^uNPYeWjTgoh9>!O~s&1rXrM|CRf&Ffq6{ikt6Ip+6b5C zm|@+R*kTaQrpO6x4#E_T>?xy47-X^zH%}t(qcO6-ZqyG-BULLdAmgKjR!98-<9-y$~KUvzQ2uJldO0?rbPiwlHQ$*N~ z9Swtep`18-yM;3p>EXs~cdRzS!gU6m-9AM|y1OvZ*Neo}%P`OPz`W4oeqY?(!^+Ps zyx;hSMe8f9(l7Z+3;|qkasBHWm3A`e*f4y#DdU@5KV9-i1K9b|B z!>kX7veMz~!NQ{lVv)>fuSsJbB1L26geI$3o=4p*cM6iBlXq`pihkN*YI%*kyC$^u zyy|(>z=UK7d$L`T4BtcSS)~Tu5BHbM{FZLbo~ECKs<_T>(7cYI5XGtu)1YkE zPW!-(YY2)sZ4_Dzb;qD0z|`4AFI_}XRCltLIEWaMMCxP`9fjjxK*Yd5g5jMKTVrke z*`NKv>#4+yV${(KEtL=yTww{Y49Y0&u{FQ>13|%MpcbWyyx?!>?9!|22nw!MwKz_m zr+!gq$8nU~{Ekk;HL9#WwW)dvpIeb6R5Jfb8u%jkUR+2^OuaIX&n3#9)mdAm9qEll za6KyWrc&;Vd+VrR`kD0VXCDP`bNC+OxaU3RGx+q_H1n7BW5WXq;N8Fq3mk~2~iWRLjAe^)tfXGDWYRZ72d144CD%n zimUFK$587bX0MGM)Ma%T-_;boT1okjJa1XVmj? za7JS3nYJ2g)P8JS=5aTT#X7KXYLG~NW|Nlrf6&NfI1IWds5t|Jg757nPU~L2{AB}s zw~P8yPKRDL@M7-J+FfaaY0cpmuTMqmn;JOT>l>;oc*_#c`#kdHd0fKSD}>Ul%fA@Bjb+07*qoM6N<$f?j6jK~#7F?Nz%< z13?r$t78x!5D^8jSsNQIf`yHs5yaL9C|XHC3#;T0vVMW-L{qF*iWaf4_E8BI#?~gW z&_Ylb#3qTZcQ!(Hc4xAA*y({?*gJFYnfsWtz(354?mTru3*2F5!YGyEfE_JA0mhTwS{SA?DkdNc* zho2~Yl5C8`MYemC@LI*v?HuZ0EjhPI@9M literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general.imageset/general@2x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general.imageset/general@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..73bf197a3f71c6018d0fc5f5098d3097af7b93c3 GIT binary patch literal 1034 zcmV+l1oiugP)=70R0OrlL<9xnNS>nL??$by4e92 z7<@5dG(MvkXPeKoFBIU2?+@af54!hRJQZ@I}g5J z2wrpUK{o5c(p?YwM6`k`+Xe7XW&u(&_-Yar5>y9h??Q&}z|y=65fP1`^{qv` z(*g=`zzVhp95!Hit_nTUo1pSb2Y9=JcPT&=FmR#G9`_>5P7tjPzp;vc6`>z6R)uu- zq0L$|7xZ+q0O?dMvQZI1qm^z?o31Z2K^G;ai-jnCK^HBii=ikzL9}L?jdvF=rb{3y zH>IHpv!8+&@IPAm4Yv0t#V9U%U<+ ztco4?2&T5*K>d?t0f?tVJBAAy9JKnBT{My$t1>2|wjYmB%x8N+R1OOvd<{|FQQq=Q zcRC7TMsQPbu3uBOUA;99Gb1-(7Jn@8M7fbHmczOkIADmbffSHh2egdUMQ=$q1uO!_ z6)v_-EXc=-W?adLZ4*zq2N=zkxAttRp%g)TCk<(vJf;hI8>h;!$|@*;tnnKMAE5ZQF6~pgJsE5wUA`zDz$tCCNrK$5F3(pnU~oY}Y5y?rK?Ge(8h*&8 zPf-0Ko1{%tls6sJT>}j$jx$a0jQa5QQtyI@XU|K(*$P}HRPVkwx1?&O7t5sw9_dWI zz7Z4}v3Re6o*?qdEv$)MhN#n0u--|;e_X8Z67%B}^L&ULZWasY=x(52Fuv9)&YPdT z_I4z>zPX6cWHr(wN!pqf^=tVpqJV^@(hGQO?|=;y&QJuP@G)Aqf+(T&MFmQRbu+Fw z)@PWW@VFl9iwF+NsDjk$2+Pp6Kj`)Iix&wA2?=NT3DC+@gah!=)&Kwi07*qoM6N<$ Ef@7t@E&u=k literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general.imageset/general@3x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Color/General/general.imageset/general@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a9e144486dbd54ecbe3f61b3083e2bf89ef85c87 GIT binary patch literal 1516 zcmVimkAvPy$7q9@>KnCV1)17op*L;cjp6Phw#qq?04Q@&-;Ae z0djJ3a&mHVa&o#6hAgqN;iF#n!J+~pR*?T=ERdg`7hrlKKt|}Ca8jfv<6zF~i1?_u z6JPPnDmVgh>suHIE0~@N&;i;j+{#8l)Tk05gpz+&5JIx0c8k~^;mUVKjp9Xceg$d5 z*ecv`z8wOlE8NXB9|P`<)CFs%4FaYo+^zMJxcZ!Ek_B`OXAUzL=aB;Tg`-;Lxhod? zu%uMsMH9AbpIzaoF9YA`Y%Oa66Qgj%CUEUi6_#*di{+1sK(w7X(FiW4I*dQShj(US z2|L1-?}`B+Tt(J#*n+%Q3nXu(*_%e%#@Qk)HJ!;x0MkCPKzXAg!H z1_w}hZV)!9Jvl(_;4c_b6BFbS-3hV>T@GVUn6Pku7_+aPLmDi8y^HJjjpnxngf6w9 zX`Pc?ox-xVIU;~IAWhMT^QTVA=2Lm}GYlBUh4*&w$;W}}33`3pfNMN?9QRKnZAwWV z40DTG#g$7Y?A!R&OHYrfNmU=$V1wb41zDRLu;q0a;|-n=%kxS%rryW7?kEL)HXzX| zkA6bts2|V>JEu}qy{S69gr0}JsajG^IBKT67v^noho=lSS2>hZNsKghSh zjW=FFao>jxXtkvi#0$s8z+gH1Iy!U;7(#Y&TtBu5a*T)<&f#{V@g^u%4SxRvrlcD0 zMqvo%Wp7tuYxmo?iu~~>f>KAvhlgR#*$dT>;&<`)$0K)Q~l-Hv)D0M zc-;E#;nU;IBgfY8$hi3b#0&R+PpMHa}4Deov0Jp}uD5rqHZF*&kY zN3od_>cUaJad~}H1pP{D^39O9qt!|VMG>pJ0dycoz|mZ4gCrrGwD@cINA!Yx9x7Mm zW?F?tBxdEIe#o`DyVufWvlaXRV`yK5PH~;W(pz!i;u#oNyvWmWLex0&co0-O-v0Cp zeEZuzbon!k@Y(l2;;om4L@hpaKGB1jy>B>cqT2TUq?WbS(B|bnFm;zVY+kzk+J8a) zT{4>&tHBGkZC?88adK)B8Shr1A81Q7g07Xo8z&ImkHO{<^R?ab`gVqP&zMrh%;gan zV7>xe52s}{?^bBH4Q*92K0OKLwNat{Ho6hcMv`n%N|tk=eF8kvvVdj-em&{FuH?y@ zYg4O928;G1?saA|@lGIniN7-0-q`}O47CpVpp#=aV@g|;Xtzz~HjdCB7EUDDg4B%N zyejn@VKfHC<>$7*#Bvx-a4m`^FN?G&ug!_)VtqA59jaw^`m>f-N17Zu)#t>KmK8oR zUHvkHqqIvv5{!qpZ8yu7Uxju;0=gK5^OnnVYspHgyNHu9o=#WS`IXO8wIXO8wy~E#Lg58xV SDZNbq0000 + + + diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/Contents.json new file mode 100644 index 0000000..bdc69de --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bus-general.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bus-general@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bus-general@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/bus-general.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/bus-general.png new file mode 100644 index 0000000000000000000000000000000000000000..4ecb6ceb76cccab7445a422592274a6b47f4dff2 GIT binary patch literal 326 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|&H|6fVg?3oVGw3ym^DWND9BhG ztg=VaE2K@K#e}8tin)wTF$mL1FEED@3 zpUgC_Y(M(tQ|Q`RlaGd-a=2J7nGeRZRB;)agXzopr0ExDNGynhq literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/bus-general@2x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/bus-general@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..74f3a832dafd7f17e3dc5511a51b2602da5af077 GIT binary patch literal 423 zcmV;Y0a*TtP)-FZ z#mU+>m^jT6T5ds1M+VdRwv#%|2>IaY3UMeu+Z~`+WAstrVf{&TW~yK&xsR%xd_R z-|=p`Y>SaD6{PYL5E*bpIB`af_c2%a5W2@Bso}RC~AH RUHJe2002ovPDHLkV1li(u+;zn literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/bus-general@3x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus/bus-general.imageset/bus-general@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c0d16724f15bd1d4aeb5503c12c3749b3ba7214d GIT binary patch literal 603 zcmV-h0;K(kP)b9*n6euZ`BSLr zbOzhhmoTI!JyY|V?y0US07X$0WdR{C?bj+MlPZd&JW0{GO!HhK7xvVxqFibN9AiIx zpq!ma1gI5@bmCBQl~C9QDVmo<6hYF9;ec!^28ap^4HXs|Dl9ZqSZJuQ&~V!; z5>=4A#3%yd3Yd`>H(|_mbEu;T@@tx*Yb-QW*ak!$6Q-YmvxZp*DSTlQHsCuGq1P~r z)ZK7>D-Lme-uP}O-PDZH06wq-wTRA#Nct6*l%Lx##{9n0_L(NwxYwF3A&yg#j#rvK zeOl6ux7rdSOPD6TO>bh?7?W88(n-nS*H}g^L_9K$ley1aqj|COxm~kVOHP_x zDgayVYX*q8J$KBG-WmG34-XX<8Y(O_G=koV-9P$({gi@*!f5nEjcl2b*%a5{gQWkt z)0P0)RHSTs_(1dqUwE=56E^0@Ut_LW!F$?*FFe_j3A-^>`l$*0*4L433#<5IJlV>> peVzT^1NM`86|`?#ilQk0nlIdtn0N5a6juNM002ovPDHLkV1h^30%-sM literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/Contents.json new file mode 100644 index 0000000..3e0c434 --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bus-general-20px.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bus-general-20px@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bus-general-20px@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/bus-general-20px.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/bus-general-20px.png new file mode 100644 index 0000000000000000000000000000000000000000..19359be4a3649e93a6cc80c2804dd4ae52c667ad GIT binary patch literal 572 zcmV-C0>k}@P)X7C6mdVbMOD2x#xpHNA}13&P%MEl@A zzd{qCZ6XAb;hDh!d7mooOR;74jan-OIlX(7kIwEsl@Nk!(tlHb2JbO8W@~pOsGJ13 z>q5}U`7K50ouOm!V-sdq4%N@3+P{VOvV4nLOkZC6wj{`SgbDRYMmk{ zj2H?{oM=B~vZ#$i>*HP=Yu|!WbU#gFYaMOjZBlfpTA(VKJu(_e`~}9hv9gqCdR+QP9OlEvOP>^32aiI9`7Uh}%0000< KMNUMnLSTXb@b?`6 literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/bus-general-20px@2x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/Bus20/General/bus-general-20px.imageset/bus-general-20px@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7ae79fa84a5f52ec67e898c45f7b08d4775cddc0 GIT binary patch literal 995 zcmV<9104K`P)b<2!bCIVc9VTpb^ z3qyzyV1?|PB2C98ESM1Z3TO#TKTp61!jtssb_m(*EDb(aG{*HvCVeCjxLCyGxanTa zwPBeBI+{a05Ms@(p|uTM*_tJ(sArGW9@#`rEMjJ4O)aJ(zPW^Yd96AzOpnf2yII*l z`AW~X$!ifbh9vah049giS8r$)#j}~c1{%kNZHJUoZlL{I!)Q3<5PR$rN?-BH)&zvL zie{3t=J9eY=A0G{e6?;MpzYBWqnd6g*oKMjF8S>T^UoO)KThfUuSzUU^RdgBk9y&9 zTVMf}WvDa{o;`)paSQ(g8`=Bz9nzWeY9h{>D9^Av;V8kBe_Zj?P z+_rz*@cWP0F;CROdhiZ=(e|M2J@vyo+6%{Bo4fFXH8mo?T}_c}hS%Sy9mTnZE-7xA zmsJ5#DytT3H{HFu>Ag$33;@QCHmv2b?Je3=N%@OEF zux#w`JA7d36e-DCNy1fD`}^d6JY+noDI2~U!s?Sg3aoZ`#v@E%VhUxKypFp{GxGW{x^2?01jFyAC5O6YTJs8Vpu~kWDGG*5$ zKYXSL{iRYvHKQ962odca-_+=y1{2bPJ#3yz@{ z`n}iE?6Hew=r?0-3@d2CFH}_14B?`m%T^;_d=>u9P{ zL>dV$y?Db3acDPEx%6ZX2nh+vh_-6oc&G2p#)*H&vpd`FIv>gLdUrkEZ|2SHdvD$f zM8)P#8m(pyQf45mX?o8>A`O8|*Lf0k2vMiw4LY_DSPpF4MgD375g=d^7I|<`Bo#UT z6$aq|-Y#r;2l=Hs3}Hra^~W3uEQc343>Gieu=0h2kc1N)xzHX2nLkiM;rbl}B$(ju zZf0PoHeZ_1UWC{}sRz3#nxymi+JL~_O6a&;wKB{@tPv@>Aio(4W7_a-rPx|M+;7h*fvV^)XXTS zW%Q(FTRVvn%SgIbf9MTmxZvvTb;Nhfjv{+_?E@KX(mdS@xrrp>$r8R=tn}{g7o48#G+?p!KmQczsWXU@M)Nu5eyPFzS2t>;Th+>v)Be;wX?a+OCY!S#MlP1& z-&p@R$Q+s~cYZqVVHNp!HE1FW*N(R9j~iqo=!oqPcM_;C~)cUL; z_yXU%^)m(P4)VYLj-Zr&b1}1X7c)QI4G{2;fYP`YNjBCZhlkw5%oc42rSjmRdgr!w z|NIN)5Owb1eHdc?;=4!LkK&cf2nx^k47b(ZHiGB*g8%EPdLb$eHUjbvN+meA>Q|`h$FpuY0v=-ywv|zvPTK>Wl*HBD2f;5uW z!~3w@!=xjvX-!oy>2UcI+JYzDtd%+YYfK%%Lu-xn=U@=_xPnmoEC-`V*5-38 zm^1U$ZAUku$A0smTL|wD>Qj$uQyZ=dC=6R$!tsA%T-fhzyOVABbAB(JFKw{Rt_q+2 z_2KGvO}CfX+2AFSi|5|JJ~{jB3u`WIM$;x2uobT5yLR=R7n4J-O zYR+UvByxD%atY!HM(4CxbjwSI=rKzVPfp_TB*voSZQib5a%iHsw3Cfn6-ZG;oZty+ z#F*_xFHOr0OD`V}6BiWl$dZm&^(_QJIVaXCZ5hdhX~WS^s+AA&2tc@pJh+gJYr9@n znIa42$fu*wOuGq~pn5Y0p-L8WhynvbkD7u>l&OmaT6mrhE#4QoM~SzJ$*0uJ3hqQb zC-F3^k_rOT^iy0slTk(7-B(ZtSH42HG$HP*v@vH=BYG6?1>sE3+E`MT5dZ)H07*qo IM6N<$g1V!D7ytkO literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/Contents.json new file mode 100644 index 0000000..517cff1 --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "general-long-opacity.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "general-long-opacity@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "general-long-opacity@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/general-long-opacity.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/general-long-opacity.png new file mode 100644 index 0000000000000000000000000000000000000000..4c16d83f181a1dd0b196af4f29a1a24e6e01f856 GIT binary patch literal 601 zcmV-f0;c_mP)w<1|IC@QxdN3G9e96g{ke6hJVoM) zVvs=w8Dx+_1{q}V27;n=5sSqGId8XfAL^T_P)D&nVCk+96L~3nI;^(kGhp;jUwf5d zM#7;Nf%^JKXwZYSv9X1j*%apH7i}o?eEsqvQgr$i?eFX#h^y_aqs|lUQ4SgSj=iz} zL7C4@wY9bc?pNQ#Manum7>tkE$QzBa%JaTAJ%4s7M53Hzi9NO-p#*zjQi-O3sy>lF5|z z64TN&a*^b0%Fa3tE)(U<%`e#0Mbfe?d++eDn~HD|Ona0ACH&Saw%pI(eO(aBe^Yv% zLZ$WnkzUQv5w*d7cgLrXe~)ipqT?>g|Im&7?hepMF;^gCc&hidQ5~_Y78}K~RTBS{ nF~}f;4C0DnkU<6+yx!mu9N^vlh~9wm00000NkvXXu0mjfa}g8^ literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/general-long-opacity@2x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long-opacity.imageset/general-long-opacity@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3de659141d68f61c2792ea49a275e36aaa51afe9 GIT binary patch literal 1135 zcmeAS@N?(olHy`uVBq!ia0vp^GC+KdgAGU?s(a)Hq&N#aB8wRq#8g3;(KATp15l8$ zILO_JVcj{Imp~3nx}&cn1H;CC?mvmFKrV}?i(^Oy_*bpecewijAtEBKUc0dTcdNlHm;d1?Fe z>UV8dSO2#P6}+*x^t_jD+FXgui5Vv~mR|dR{pWE$P4+kU_N_l!c(>Uth12f%t1Q)@ zrK?R$YqdHnVuU#NH-B36A*g7l;7Ojl)6d+kHZuE_!}LuiJu>U+r!zeX=eOPczhmCb z4E?Le-!@PG_hIjutpQW+>?w`b`xejlV2|#^(EsIYEkcCuT-m(c`G|GLw?(aMFa3L) zw6goEm!+l%*QML#k(a{re$1EE)!zN@Z*e-uk?bq)R@Z%5vp09+(Mds`4d1>kzj)^M z_1j{5?saA^wEeEK;p4o}HpL3-ldbU!Bm1jn6ntR5+TnF%#=~P-tmh9I2iD9^WLXgZ z%k2KvsrG4?RmHl0y0%Lg=Vkc3T2LNex6xshY=}j|>}h$~nc<%8|1(T`S>ypQF{L{Rvqc6>mUV4Gy@btqavtD(p3w>O0 zbCIFwg;mn^=NB%xHV>HCTjh0eYd{qM@;cv$PvcB z;8dSS$eIt!CVYzOJhh;;YmVmR>L|bSO^F*`zi{YPf40SS`hs1H-f%f|fA*Tx9i{5k z*7i;``qd;ew?A){bG@yy7+jAEyqVsSy~o+AHMwyTPu%6^H_9u^4Eo)V|M_dke0qJ5 z|COpgcDdnGSB7MVat@qs+HdXn_{Kx#pY!W}zMc3s)BRds>cMHX$&Y1L7RqR< zSM-YO@truZbT#9~8sFz#pAUY$pqLeWUhj?E?Tr!%o`+02cI7U75y1X<{l#xSGqzW!&oJAKCtZ zzf^$lVA=Azg#KUh()kD7mFD}~y}E73`+SPu`K|7MLzcS#-G1o*8ztg173aA**QdST WE-QK4(H~eoFnGH9xvXuA|9@j6}-V$woO3bDC{#T>8xjDC2&&+~kqKj2Aq!8k&|8ejkb0C9FgVmI6X007!< z18pQ=igoOUZI5)i5DftAH~%Rh>h5vBpIbE6(GI}w)cn3twglOpu>}CiGZk0-6#xLx zYiFdbTO3fr912zO{H;Zl=Y#0e-3@*5L=cE0s0Of(tF2ZOb`X`o#lG3tQ#IY-9>uyT z+OoDF-J`ql0P0jk$ZHM)>=5zM7ukINAML7pUbnm+qAP7H1IOlTvzTwi)xxgUn=>nZ z!mK=Dhv8U3yhv7{KzgLgUkOaG@2AdpItTCZ)Kb<$^9Gedg1h4w0v%%UE^CVAr0h zxug;Sn`LsCV!lf1SpWyI@J3fxn$-$r7thLAbcfu0p3X?UH(ivtZcH&4X%6_ek3x2R z*OjR$(j?az563?XurI4OV8SFl;9jn}x+PWLpdiXBs6QaOzr@k#9p^L%5y4oaLARtX zPJLL4Y8#U_JYZ$R*t+sR2{F}{VynT$(c3XLT&x!8W6dF|xoTnk1xpC1xcC0)svn=d zx1qzS^3uC0yzRo%2i~9Do%p)l=-ljgOb9L|L0?z3u9bsG)}=dbNnylKPp!?E!d+o= zk=uGsa8($&$r|^)tnnDAUe?l>wKps#vuky zC1kV|-R$z|Q2VmB`El@U{wi9-2b2=*?(&Y#^v!)JwAQY-nRtRNVU@}=6dZB-<}Tj+ ztB(a7{Y0cF{c_>0Er;CP$OtV}%5+6%h1_jZ+7cKcT{!Mdy|H zbu`>Bm73_4-(`%)W`-9lNEpu+pC5JEsbCZOnQz*6wK_xvHYa_6_is9(^69$Xn+0My zcQdDcH7man!#r7|D|Mfq9g;m=g}pwI$N7-9c{sEkX}T#cJS^_ydY9+yvZB@#u|P7= zoNz?U81M9rfmZO(7~NYp&6UYY)pRWmk*d`wJ@_#z@{cTvSz9~)(kL=6LQpnC^b(85 zZ+mnr_563Gl*KvkUGe-GmZ;;I)A!PY%>mhQr!aU=d!WD*s;d^w3Wq_KCh zmVbOy+vE~?wIkyRpRXi4-w>y>00x;H(FkmPH&VnVGihhl%S3GT0ZAz7hvM-rdS0*& zb%-~;-Gs}}fUvnnm8{Y5A3v}vPo-X_pAiHG23x&od(Owln|Cv79--WtAq*-{Vo1_iimY*1SlI|J!tpf{f~G0m44Q)i;0c>SX3WB)2$4zA?wOvA6er zW@)6}iq-W&my<4`9aH^2C49ecMmL>H@#`o{gArZHl~8v_ij#| zGyR6O)GraC&3&B{j(_2ES69;w?8sjwAY5r#9jSMj$0Eh4>#Vz2&!1Hto0O)7*ojH3 zHa+4}u6oY6$N5FE#Ik)pho@dj^4>coGA63+Y_6m2+GAg>BwZx~t!F0*r9D})>9X>h z`(I^(k1ELd^F4X?%weL0hUp5~ohttp1!q3hDgN+zmHG+;g5)lYTO;V z{p-wDw?p650^+JT)6$-oWJ)G}-oE&4~nt-myVqt6$`--d=8m6v7OK9lkntX^++)W?iVQ?I7E=m>DQm_ExgELTYl>YsTG*iI5xA+bZM-o zzE#!iqo20C`#<%FYeI$h)U9>(zxEA&Vh35r^`AUN>`IvQ2L` z@4}Xeq&I}nHgBn=9WyT>m7M!G`r-LLpXd1lo+s54Ab~xbx0Muryej{!N0D!(I zCx=rpTjVAF6s$?TlA%rk1ka;cM=U#)8vD@AtdsNeIKlj!e+tTSs-~+xTf62_RQNBSBt+cJT zv|Y08yV7e(W>)FvB`d*3>m-*cQ3l&U2X|}ja3t#f@VFJXz+)%@v#ihttzA*n9$vFD zl~H7&zV-qiSLVA?By~B$C$7$|_Tt1qaFb$ETcSS|XTmceoy(2TAuC*MC#-coC3tqM z?6)bs8CZWCpHpCAjg(gOt8s0cIB7>ty(C^N+rO@xdu+`QGvwZD@Awa|j6r_d7i&~P=PPCBgx;as3McaT$F()H`)Va-_}Q9n z<@7W+9{y1JUekQ$o7b?J8-3EWPXwWtY_}ymzWDrKZ3{g_8%Txz+YH$ti4cpKT=_iO zvSKMkA`Tt#GN+yDUJdw?abeN>MF>_Hn#vwrUtH&4tSL$NjSg$S5%8WN9HCb6@%JzW zx*ef1&lDdM!;lfHEFT;W7ea@ilG$M^qLKJV_fn+Zk>rtr+^2JGS5FWo8u}5~){VF> zYNY?n`%9_jrfyW$o%)9D%gN<1d`5IhjDx{4Llr$HFg1esGxZu?tZ@?D4Fd+do49@? z(EyYPf!Fpm8-og?*c@zl<(cU3QyiFr%*ach*8I2pnHo_uIGi_d=!m)b(R1tVljRvH zk5&65=Ua3*BjJ`qWXOdCIs8kTju&hIt7oFuo3<#_{jS0EJag^JweOAxp8vhpL7oW| zC5gjjH`2JnB6yv&k#?tQYMJ#2JzsQd5iHoz--iPShAEABEcmuq%17L0UV!#1Co>4-(x0Y@xX3!6P@tCVLO2)@Th(Eo@h<&isT+)dp>z!07 zbg<&0#0i|3OKWt-+NnX?qFb(p9hLN|?4idg4~n-QHX*3Lp5xYSRDaIjGxt}G*D!Ti zTQZeK$A^TFMXjz>C5u+sapuz0f(R>&3_ttJjCrFLw)u-!NQKYIu1R}YxD{~Y!Z47X z3xsy)gky=627ab_56H%Z22C`KFBlnIs!xIzRnvR_Sr_MTRpkmogyZuM%Y$Ei!{!|e zh)~EX_~75ONdXT{L)RfMNh=4?tt8B->eliIOjFqlT%TUIF!VvZM)GXduaZ)>@gaDG zz~zlVw#N^hxm~+$=GDJrvE5H#*7b+YejA&h(6a1$J)j+UoEU0w+WVu?-D+m+_`u)8 z$JoxS=RU=Xy11_hE=DXLQAtDld6o_pUY5!WPpljrGAwDMxJDNgGVJixriezy^8tl= z@N89ah?y*x8_0~_c*MFJv6_1obQVs0W=K#FnK> zBzvjh0ST}ZCsCQ!LbtkI#!>HRW<{Fh>Xx!&TFtjny3#6i-5n;8F@fd^GVaH^O&nHW7B#%jd9MMJMt*Bp}v;nWMf zV!nFIsCy#9o)=GRRCU(%fBd-<`h?Uyr2JTIoNrIG>10s#Xlyrj*cc)w#=|AuB|xz} z;h5bvq~FS#CnCFwpR8YW{p8SoE7ju9{l94XX7O)FO@FL* zFfMJ+#DeYJLjeF4)Bg)-lYjEY-Utju*jWPzgNh4#MF?$m$qE18OP9NtrYrl#BM-F4dPr6zT~)QZD^ufq^;&(UCgKZr z*a@&LGFN^{*k0SAHU+Couy5D4mwRBP`!%*v?U0>s0I64U45b0NbdrTHLBG1ykiTnW zWM<5s-iFf&UDVmJb+dVk)d)1TBsFh#6b^OawzAfatH?{s*sF_y;c>7W(#n$N3q3f(}O4r5C!+a@Cn zeW$1*X0$}dz$2e>ty1gC@e;EiJLK!}Zbo46h>yekpNpFQT(x{}Ws4l%+|ZiZ&lmD?wwx!;g%Cco19*eXw*j7=^Y@lT zOENtaNbS7=7X*vXuWptQ0~q8ZqIDuM=%_#YkL)fz`Ovw3(Q+tiq4xaBPuFRQCz@uW zHNF8S&yJgn()FE$y_`okwoKw%n1L`mVWlm0=DwxMW8~px{j+DxOEWSs7veb?AkNO~ z_dUvW8d6V>t?eu}EK7`Q^+J@Qj@c9%UP9k)aWkJTN)dRbINA-76qdB6PSh1W>5xje zmco3s&1hL3L;K&_tgtd`hKBDvI>er_i?7q8v?Y6+2Wf*B2tm=^eSql~n`6~}WID6U zya~hkP4|~hzz^|#B~tv zuGYjM^c82wg`4)HzWBvEE85&{lVz=MGD7A?LJd4vM_S73@bJ-&IWv%ki&4nJk55mj z0P3oi{A+iJ(bQ1|?8fdV%ZV8YTjp9KHvjQ+ER^1BD~PH71X=n+eAD+v;ef2cDyp|wA#f! z;_uh}NjKQ4VzC>~kywXbQk6oTSz%4qR{ug%i-Lch8@D__ z&ZinRH93B%jI{P{K)Pa9$K}ms+2TDS?iA|!(!TKh0)bM?L|IF@MA)OY?&9{#p%!m0 z9*~aL#s$iBbauv3n~k?5MDZZhZ9iAj4%*j3-&U8?x(`*bl$)7}M`w`wxuk+OrWuE{ zBJTU}u`Um<>t!#WptzOttpn7cmpF+=XYe^7?lGO0uO>~?kpM@PRI}(CLq6l+dRO%sufk271L2G zu!~=Qz2Z3}%*b54;w4gWH-xYgZ@opYcM?{Sj3lq?%SZuGvsJ%1>^L(z`C(*S_}K;9 z$jaB-v8Z_&k|!>8haoXZXj`_+BmrobLk*7fDaZw$e$jJ0mf|vEJ79Z2Trp@IBZnKyjt4Oy}K6h4k2Vz83Kx%IHhi_j8d{Bc7&ix`HOu|)hu^gq^mIG<*W>meUp5P(T!qkN1=+jxK2rOb9duvbFF5!FzjO9j^ic zFJJ90`1vXtE9!VVFpm@9=Zyj$pk+Gp`g3A=4m+J~{aR71W>~WOg7|m4i*-6t>ls(0 z?av_UN&20^qaS9)r2P_8Tb6d-N?XlGRWW9FeAmOtKYvpz**>sDMui-;erS58yS}OFsyg&A$fcPT@#%fAhh>{^$WsIoNFad(5=bC{ z1pY(t({( zsA%*K_7B2vcyyx6Z+V9X`+ONs)3Zw>ODh3!A2Y;v!acM>2bRZ4|FV-O6T7`V-L}$_ zV)#ih;{jF!TRJ3_23}yAEFQSo&gnTvZaj&_P&_ z83z0&v^hLF*0GDYrfK@q>+2XM;U^gP&;|svS})UbQ!|UBz=Ty(EKfSq^866FxWW*n zgXgi~f&Tf_=|3pdMahXX!92Ar)#OpZzz(LjNGb7YTDP9IWjW)bpE3doB#=PJQv?!7 dAc21yd;@8V-TpHmZ|DF3002ovPDHLkV1j`4`!xUn literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long.imageset/general-long@2x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-long.imageset/general-long@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d775bb74bd9f7fb8648fc7093384b575b075adc8 GIT binary patch literal 1131 zcmeAS@N?(olHy`uVBq!ia0vp^GC+KdgAGU?s(a)Hq&N#aB8wRq#8g3;(KATp15l8$ zILO_JVcj{Imp~3nx}&cn1H;CC?mvmFKrVx)i(^Oyl5>>r9(C$S&P>)@9$ zI^roIq29ACS#9&oqXwL3^28=HI@Z`HU5Wd}c_jaVUdAKt_#FbGS3(sowsOsCFRN)T zF}b0oW~RcmEo}Pp_`9OLQ~kncAG`WrCun=n)ZeeG-|y^qJ??Sih1J`0zxE{eP83Ky zY0lTg!YQQUF+stR2*yD#w-xz5Wu>y&FAdAy8x^K5yjQROQ$K0nReSlXuUEwVFPwX= zudlau>8n|yZ)_@(x>euFt=;c<$n})O=5qJnzm$XcZdCpVX?|;wzEyU0f&9vO>L25! zmu~x3-RZhmsQc{qd)3ox^^=nyPm+?HdS&u*Bg>CzT<30(!_l1vV{m$K8 z-k-H7s_=At{V9H~*3R~RxzlG4-nN@_QRL>X;@`W9T@U`ftA5+1C_ON4f4iaZ*R}FkYc#lJMD9#fNpkP)XIpMXF?9Y*(YnHZKT2ps-=8wA zH2t&>B~a+3Rs z<1f9O-uQfLYvr4+-`khk@41?CaS>~yxVV0d!dc(K{5(s9tG3Osb=#EHvJ zC+}>E3E%Tpd8MYT8AIz)fgAdQeY368RSR?sCOMhgdheg$#s2K$ktN&hmnBq2&Jx}% ze*N^WRcE#Y)#UEce$&e#V_Wd`K+8Nwg9SfSn3+9ShVW##{a*X|yk+p`3mf15H*__a z*4kU_cX7RY?-PShhmNw{dG-I^H*WFs^H2WyegBpCtxMas^rePft~)vDXi)0yAio9D z`FCU{EsQR&O?tcTTj}+`&l{bu{Ck&t!?R>pt3|_N?m4e6c_0^@zwxu-umL@&||M@{O{;SdO64etNky6*%Zx8} zkYu7#Jg!lWCF;o>&lp2wv-arE=!ehy^ZC3!f50cx+tW>5Ra+GV0;$8@&!RTn3eA5>%`TeO#ztqWv{rJ@IUGbLe z*DreKo&#wZ{}p+EKn|CC={{b=!f4t0gR8P=4Ju7uB$=5xCw+_5T||g?NyVOFs+2h0 z1VmP>p<7sO^G@`*4hV4)=rl`X9nq6ooA5F?UpUg4a?xe z&6=WgGrN^3;Q){88k3(N;_-`LI2zu9 z9(KrQG2a~NJY}REB_e$WueCx90*mZBDQ6eduSRu%I#aK!$Zt+MC<}i(=ZgpOP;$H;f|uCi6()I2cD>4eRqB`0;eCEr$ijy z*Z+wFS6W*&KWI5FwsvgFr)L;ty+e2p^`{+h$nI~9z+*mmjBGP9k(T1;O~8E)X~}CJ z2BB}Tsg8;q|0>>1T{X|}C?J^P#!F_noN12%|HrThznfs`P@g=^w@=EitK7L<@2WrC zcf0F);2T~6z!#(Nl-;z~x5Flv`CcwH{B@$~1a$?PV%<{nT!ieX-I#l5yQ;o@1#XQ% z1g+sqQ;9t=_!<|72T)3ko(0Ey)&|d7%fA)zo5+`9tHeV6ydN{FoN7G6Um9vS8(M3f z!B%SLj8sOVV~H2Mdidy<&5eFt_w;LDHn3=ETMDs5E-vS+nMdG$imMovCI0bZf_D*Za#nCWEvk%m>B0bukrL7zY@SBZyx`8<0#7 zbi=1p`v9BE-GXwf<00SksrlXAV|n&LI{7auBi-DZvA`ZG)}Al7 zBO@u@y>jhtDQSL(0pFO@^e|#zvfyOG&p}ta&MMrlsVj-mANv;P*(1F1V>Kn_<+%b9 zhEV?!hxDl!as`etgf)MaRgRc3`hV&)DLh8d;_4rroXvCl0M!u{9 zao;GMzzF+3SL|d*p2T$!vMs9XY=N=>WlxWQ?-$vm_-m??y0#92`W^M43Ehh8A#(ur z#15f2`^9uthfW0A^3ZDIs@Z3^R$He?jfO9FUzMa+S-!F}bJxwsnS!K%r|jHC7Va~R zjC`eCPaQLr*bw-lI!xuVpd-d`_bE2a#m7eTPe9+@8M&2S@;BUFo^bQsKyaAnSpa(B F#{chtV%q=! literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/Contents.json new file mode 100644 index 0000000..654b1db --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "general-short-opacity.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "general-short-opacity@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "general-short-opacity@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/general-short-opacity.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/general-short-opacity.png new file mode 100644 index 0000000000000000000000000000000000000000..f91735ff4bd3abaa081c8cd8f2380362c5f49198 GIT binary patch literal 589 zcmV-T0IgrPHRGpQNfi${ZLfbsq^tyojQwVD5T)T|6A+k+Nt~$jweci z3RIv175Gd*mb(ar!k(0K+Hnrit_ze=>!|o#hUpua#`H`Cr5{T0O74+vissewkMsA>Z+9kG zRuSH>Uu4XG=h&LG-QJ1sc{AnDh3U2Oq5Z=nnwB{nXCxj^ppbJRr)V@Lz1T9>wNxOw z8Z(oYg~voWYwH^(brClV!#umZ?4lw(1k)bnKrpxUvMtv?INAv&+&87?$y8e2AMxD; z9Z?#bceOP&3}4?qN7r4H?vaiC?+s8%F`FTwIil~iRva*l8WY*F6_WZvQlJ7As6aeX bF9^H>_*C7n?GXx=00000NkvXXu0mjfpA!NP literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/general-short-opacity@2x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/general-short-opacity@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c58ebf6825efb2fd8cc85aa65663fcff5863c3d9 GIT binary patch literal 1085 zcmeAS@N?(olHy`uVBq!ia0vp^GCv>d?^BBEPmlLAV+|J#c09<$ z_2aJy#|^ei36I2&ChZl;{O>fe&+pZHX$i*YwvD4%tLGC-=sTVn%I@hzHWDeg48m`CT*1xTjIl#5$h~*c!%9Rq30Fuhn%9G z6(4BRJzj|0)303mHE+>Hx$8BK zMt!0yb%W|}+*HxeUzh#xPi67SEh-tu!?ac~A3m)(N3H6k!-4la-6~Fui|0HnF5{dx zjmt`In${eqe>cw=To10C#qmniEN<7O6&7pWT>g39^VG@(0-M+uINk3Ek9gFtd~~UT z;6V;K%c*YBv-(ocnZB^OH6wN6s%sa%e$dkoWxS9Q6IkGDSQ%JdonE9eYe`Vb^!^QH znd_NOX!AJjZShI9ouIgdwMofh*}4pK-|Yg&YyXzj_B5ncuXKH+vu|~2*i4P!KWo`0 zZxwWU-Iy;eamkiLPb5DmUVhojpuOKRj$181FWKpQ*`Q3(rC|EWLo+A7Dp@U<*BGxM z@*wxz`!?f>PaooHexJL0_tvFNEB2jkDf|8B&(A5T5>xsLdh)ZRS8jZ{w)sZ&s+#y- zkI0PB2&$U%g`K)46kbCe1c`)Mc}^Gx>n=RgT>Ub_JEc46EFVdQ&MBb@07Q!T AL;wH) literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/general-short-opacity@3x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-opacity.imageset/general-short-opacity@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0a527fb1db65b1556451d6665134e398ac4a4324 GIT binary patch literal 1687 zcmcgt`9Bj10N=7Cv#jNwEJrC8iW-SxhVkT9Ss8Qn*$_E~IdXSU%v#Akxt``Mr8V+F zk~TS0GwYx^ix_Pw&-)|Z`{8^30pIU+mos({Sv6Sz006<-+qiA1-WJ}-NN@4>$s^rc zvNPQNd?Wy%Y5FgK82S<4tr8gNW@ink=c>1 zMFJAeQ~gDUa1t3!+E!4^)8pD&p=R88qE3W-3?^`2c8s%@mfG9SB|pkqB|q605!EQ` z_X>)?P(If$Om;#ib?4?PoqTvNo#QZjl zxeq^ixLC>+IB*`OHfk8sy;28o)Ca7OkY@3IPQQT!3GV8P%_WD^IdZ#_qScYC-aN7V zXs^G^o}=37$3Sr^ril`E;p2p%CkJI6G$lGOBKDo;$Ld_>^%?h(hb_4V)1YB8XmspdN|@P`ZSQ-K$e-_y8_9_11;-Wi3s>V2;?8Q|VAkt#^q9H!_kj>nKIC;2X` zD>v`*(DX=tQP#mRk)#fqV`6QA47%$Zk}#xSq;$*l@!*7Fl1>+dRo23W!0c`V_K&mt zzqsf2Pp_6|Qx^?%f8DDgKh-8`l*G1x@0Uz~3xR9jmgLWJ#D%WIWnJbF2OF?FH~moD zkHsf*8gqc=*+%}jpQZ2qg4(61-lm)`wjxz#@=G`TB6L@ls63V%Hy-TcuF#{2LPNQI z%-j)^V~ZJF|LY>DAu}>C-!X+zQ#?=|1xL$^(!ONb%;!;agX=&Qy!si8h*qdty>bp7 zqO9`AWaa@SnrRnmTIWo-95$Ux>7-Poy;SltY-cgtPOv52gJVp@6~n3;kQivI-OtRF zP1)Nu*k1K|8PDv`$Zve`tGYTI%J@8$Tr_jy;nRSNG# z8RxxRFf&}H2YY;#jD~($!dYK4F!}=%pgh~+LkO=oMH+oX6IJbbd(~buZ{!IrPYtaF zV2kqa*YW$!%|Ga^9T=KOXdGAC=HphkL-pN`#`cHOnf2ZQ{a89^4e~ZRH053(JpwX+ zDBI?EjSkEp`ucXC46l5qU9-C8h45LQi(b*?0-w=llAAl`{jmT()i&owfHjdrb=@sc zw#ucZWB;6UEnJ>{c?Z3(Xu z%Zv_l|L%A&7TAbor^?(~_$*Q96p4TS-mPEoSC9vvj72c3Kb8^VC0sWK=8oIT;&Vo2Zu%iD@E57}}S@*~@!El!G=fy6l&RK~>Lb!f3d#)}W~s_B}>E zj4>PIszC8nJSZgzmYY}?re)Q6cWNR~)C|xO-!N<1lth~H7%zH4fw_;5k}7aV@yi;5 z#AFU1h|CRZTwg7GN4rJuY{o`JRNs2A}x*~ zGIB|cgk@3Kx`9EXP0vm(w_v@rNd5X>Cu?w@>K>`&*pz96ryDiDp^qrR9laDVdZAOF zo1cBSF$%sB+9(TY&#S{$x;0HF)Znk0&Rb+TexTFQdeh&|&NHvEXoG=TW3A_obNOu* z3_Izdk+GkI0%k15~Dj{vZkGdA_szN!BJSuhoH literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/Contents.json new file mode 100644 index 0000000..a46f21c --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "general-short-turn.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "general-short-turn@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "general-short-turn@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/general-short-turn.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/general-short-turn.png new file mode 100644 index 0000000000000000000000000000000000000000..397f86ad5b0813b0c6f3964d2902d351ec69d463 GIT binary patch literal 927 zcmeAS@N?(olHy`uVBq!ia0vp^YC!D5!3HGXGP?f8;G;R7LIHmoRbLoVpMa?Xe@-!cM{^dUNKOsNJOH1P6%I1d)ZDz<`I}m!2>EfiR zF2OB5|Rnod39 zipRPI9=mpYR60^2n6!t(Fs?;HA4#|<<7h{CV)*sjd!~F_?#aA@*{gX&yN*Gt6Vu&H z&#Y`7+b&17jpxGmZq4j%iwu=}qhGk-!nZXG)!qnI-~73&lZp4RoN~H+V@_mSZ}0te zyL32Ty_$dJ%Z#MAvH49wR;KSnS6Xk+JM{Ztf7-rJ$4n)p($a9TeszH z>+k6~y+hd zcrCW`JI`c+pE9j0^)DAJ-Et$W->BRB^oE_fE0$kf64xGhq{HfF%H$1AlTVeoTkLpk zxonf;pMNqZ&Yo>lGMiwyLO5np!bQz+%abxTwajm(-4|3=dG^P`>_79gX<-lYuIARJ zO`N!5Gv}1)aZNLCTE-yA=5q;JNuuMycFnv1rT zFs5eP&#@`&d3%g2I;Bg0&h^7hNf$SIUVFCV-yiNBZ-V2OFJzcLdoJ%aPhlsuGqIY# zgAz^OW*hZ!_gpx$-EuGA{cFb0mADdrz9}!R-1zrGfs6li%erp1DN64A597xbo;N!BZ0bqRE{`f1ed>ue^Gb)o4oKqIlnR+Am$*^NpkD zUHiL#k_I6n+X3z0RpVcsjVY_}r&IBzv?8(WEF3b!=mB}r3x z4MVeaX+<}Mmdld24q0I)`yfQ;{)&EhzR%}*{($FU`TL@^G{Kqx06+_iK?QEP&z7=x zsBaNBuf^Ijjd%>63IKr2|Cb6jaR0(qsX`4zBLNM4#-F#s_DFY}I{?s>zw;Ye4FFK* zU{UVDDJt@^usDO`zr3gzx-3`w{kB6VkfVj#o4!qc{U|mQDA{?#p9Dmvsj3tZ7d3va z==8p1@bsdt@FJKP3)i!0Iu%mGLqWYjl%v=-U4l=?L+4wvgrC|4_5vl0&?^646kYAy zV(+YU?x*$HN4|Hm!~Yg9vJx;>PN0#xdfVK|dkxf)KN7$h*ypQ<<3C_pP6JAIQ3+Rh;%S@iK{F(cn z9p=&Fm6I%rV``5f9$JtQYz-cL-EoFclX9%fcyM`0qGLMrBCG&Ycs)D3C2V~-uhk&# zAcXwPdCFJ3-vZ2*3tZDys0zky8+jHOKj~0uq+ff}QaRn%QY-yoE|~^HC!sKXQw~C@`lEDUid|vMdvHmkML)ywqL#)3pPYu z+Nos^@86&5`PfX6ARn-BjArK7QwDQ{V$8Vf zijyvAX{)bx(T;&Ta72!iH^HB@gD&c#NLfaxz6-H~viNI@A1{CBvWBO`ja z!=unD3|JkYzkaP0NXDZ>Ee#{rB4jWkeUX`Un4jWWsa-On>sSerS<26=s%DrC>in$T zL%|3agkgm*1L!7*AVxJC^p04ic&V3YYGp1mov1tby+XX{u0K-?YwRa|I=0J>^fh|) z*HV9H>ka>=w@w1jHer$u=eUpwPtkC-4<=rc?p<5;61|G~um*KKbTaGlnNR_plWVON z1FL~Y!b^5aGwE~BPa682uG>{Jzj7eWQ@p@^{%y4%8>h(C3-hmN|Q1l|C{Rm_Vy~* zg4R7XHsFzLPTwu=rW4++ITVZM+zJ!KWsi)uAN^Ps?N-!y%Omf?Q0#@>nnNbX+m2v^ zPDM?ndGKYNCFqT|Wr|n$qC;TIMcmCv^&s8q`J`##`_hNU5zx?JhYgu%SUs|ToSEq z(r4+919n?IATS&kUux;zf}4c%Bt2Zc@{U1n99|MN&2xCFCa)Xy{G(A+x(3Sa z9-Dn~c{igLoq+!L*`GOx+^x;{lyUKxengFvi$$|>vfpgqijGl()<$u!FWzrN#x{HX ztsW{?*h2P9<=u7CIe%4h6J73OPZpTEnZT_DvuD^>EX>Bdo!Rp={@j83x0}b)&*6qy ty%Ft-_w-O?=lGaPrH!tp>JP;#qtCm literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/general-short-turn@3x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short-turn.imageset/general-short-turn@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c2c3e6f63ba7bf1443dc71032bb73b319653cb9e GIT binary patch literal 2654 zcmds(`9BkmAIIk&*#{#b%6-O)LZKnoRAiOoGeTv{*O_Ax;o}O+ontXVNHG?3W}<0X zuC6&|b7hjVXpUw3`aZt@#pj3D^?p3wzdzHjIN5{56vO}k00`z_13$>H0}TR&54=%n zJL(`rLmfQA0RR=#|B27G@RZNNkuMx>ZwM>RsVO@reQdX zoStoVL{^NhpZ|dPjbp)@KyO~eA#c=LL`*essPSHuLE&uVJP9?w#tA4?9D&b`6C^lC z77oXALme*)K-Z(ODteQ-ZPVO=0@I#}R}R1}i}jokT8QeI(&E{h#%4RiGbPeebS(ju z2vP(mXBRz6rq24s;XcIfclY*={Q{ME@H3%5U7TyKnJ=odi`_w*juR_PyJjOaZLg#% ze!NV_z_XBiy;|$)i88Yvd!!pl?nYqnn4ja~j zF}6TDheR7josXUUT(f#_ZHE-u(%6LXy^$srr&9ol?oy(-^OJ z>8-01sK8s>RaRy#(8#?N%eZ<_J_XaA==<2d`MhRKVbI7_C##}iN@$QZ$`6! z(*mWFaU(o`3Dp9yy@5BVFH$2XeLEF0=&;2%$$M{`4gCw(+Wz!Os>kjy0;&{juGY-P z_m^hLMVbyEzXT+?DB9j`mt}5l)58|V!VNr`M_ViFaj@~u1v8L_t5Mj}k55l20Lr?S z{2LF5(abRg%+~%V%c(gDJH|!|rttA|43ySqCxEO%xDi}kjkvo`XJDzhRUxq6ol_ee>wmWx>vb$}Ic)jcG?gJGJ`DS*?u{nf(KC$SnY1Uz_=qNuP z#`VDsy<7%f5~s)3>1+}x>EEZ0@L%A%JIe-!Q!;qRDafziw)e5GUjo%-MRnAQ?2}gC zta*(H(X*GYdJ7lb4a4sxS?|yqoP|^*V@R9&GExBKe9bR5E76QbdJvNsd2Y!rruxlp z0&-D?=!H$+qf1QV+gI(gi2&-AaD$`$3Ua|`Ui6+wAiIv)4cdu|DTYjfWCL4rrJ(C1 z{B%3^xi-#N&fQnAujA&^v75Cqknct(mn%PPtu&-kek%;U&nwXEmO0N>2%dU7A(eId z@y{Ahk&)`rUhV7@3J7IvIH$jc0ur7m1< z_43A^{f(gaa6zN$Fi_&uX?1I5q>?3}GYnbITke0Vb|uzy=G7dQ@eKvJDO6cp#+=MhqWL27Gm^QuLd=U|9 zl9ieawlF0PUW*H*lwh$SYk6=JN9WYD!oha1Kn7Y+0%UuWddz~$nKj*xeAJPJtIPL0 zOx2R}e5VVUgN?y}HorAq$V3c>Vi2YGlVO)O?yX4bVhhqLeo zqJCHC_|UwlbU;da>&o8S0%)2#SUylEpV9S%F6B}*GhF|JzCzo&Z0>B%b#F?i#D&(t zu)3n-?~*y~I&CSi<$RXj!*XwJKQ{MOK}nP~Kh}eHiy`uA@=FmgqG~zGYR{yTU-4v7 z2fa2Bya0#2 zvw2X&w&%3WbsvZSsOt4*Y8W85=bH&@^x=NzZ?IsEL&&y^WsI`TRd4TTY>C@N?zAwi zffv-wW4dG?meQdrLZg*kIo_}DQo)rxTl;?_f&3q-cp{KBy3d8U2fzay1i);aZ1C1T GiT?$#^U`(z literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/Contents.json b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/Contents.json new file mode 100644 index 0000000..624651c --- /dev/null +++ b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "general-short.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "general-short@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "general-short@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/general-short.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/general-short.png new file mode 100644 index 0000000000000000000000000000000000000000..4aca74cd0fd1de25d3f5ddbeec13bcf73aa09335 GIT binary patch literal 580 zcmV-K0=xZ*P)FC_n)U@Q;95 zuXeZFV~Xh?GtUebERgsL+;NjO0_$-~ozu$bZ63I~w&|#lWS8C6V`^@0gcd$f!C(l^ zFZ^(Mb))iUIj1JaTxn0st6MW$J0A8H*4S^tJ+wgwhQ~$sww1 z4}>@<&)q=1#cV9fvJ5Rv4X~etaqJ&>!BS_2O1_6mCe144hBGwSZ_Ef|MXc>@e9fL0 zw#41u-UlA7sq|;USgl;>kmo|4d_F(-Mb4-!#azA%zZ%vwC?&MH zyt-Dgi>RU~>Sr`Mhe;>~;~v@oBS!0`TW)D(a~23QYKrAaWtyKK0uN6ZB6sjLH$5@F zo;ZDm5?vHuxMRf8N)k;TWCYBhJFBD`m!b@-DO*x7F7bmTKmiI+fKaCX5%>c|4&AWz S4dIpm0000 literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/general-short@2x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/general-short@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..af05ef8b1d03b3e5abc88ebc3fdd672203f413ad GIT binary patch literal 1081 zcmeAS@N?(olHy`uVBq!ia0vp^GCh2$%Q*3+u^UmjU?sq4O>5p>Py!r)cfnDrsXNl zzwT{!-zxiF$rttBe*b`sj25T)ZJAl0e)Cu?d)?ajX!AnV#;f1uV$71B<;tD<@aYh% zd;f(nww)IrwD(`RcsIRSIY;T}b?@`0Dy(5)_F76^R~PTk&)0h@f6M>nJku#%i!N%E zmVFH@c(C{Dn(UILUtYET=2PekG_EU6PXAf?Tk_fRryLi~y;ImFXm`)%cSZUgr*28z zhNI=%Y$RGPe|&$pH;?U2Vp}tRgtCrcd0VHp>Y7&5#G3L08ycs*+|2iUX$S_@a{EqB4i3KZ>yc=!?Eh^~4m}nY z{<3NBM?ccgL>+g|86>3N{TprVF>xyyQ9_hzeJEr@DisN z2bHE@dNX&~Cga~y7Jc8A96gbf{K7F}`s_6f8Hd)N*snkRPDVzC#l_&gvoa@5wQI_` zl*c+DTB4qq9p&Su>YjIVzU>{@l9av_BUvd<`xTypyU_sOLR5CX3~FqPUt+ z!NR}gci)-VR!dnHe)^*Eev8JLa>>-q_xDr^FMVmXR7vLX>prM>E31@#DHi4yl^u<@x3Dg5mOauxv3%K?*^QIDw{Cu}qAR?1l5$`_)10|`&vKpN z_*8bswZ!QFN4@0YfB&|J8F=X2YSu0aRXfMHK1}fqL-Dda)xE0SCnK}+R@xuD>zK!V tLW4(V`<401KaWnpl2wp0(0{QX(o=;uG}_27o(0V544$rjF6*2UngEve@67-J literal 0 HcmV?d00001 diff --git a/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/general-short@3x.png b/Atcha-iOS/DesignSource/AtchaImage/Icon.xcassets/Course/BusRoute/General/general-short.imageset/general-short@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..94d163e2f8869f09c1b20ebd10a7cc78d40c9df3 GIT binary patch literal 1686 zcmcJQ`9Bj30LC|~92vtZN5m91Gs-7K-olu<&9P=$V~LtbGDjw5h*7ztB?KD+Bmkge@xOrf`G?8dBQO$&u?3WK_b+Uh9l^>q{YsO>>@Ttv z$amaNfw?@e-k9Z_`)h#+-I*Ma(J!`C*wL#o2sJXKqc&~EKZ!(bdMF<8W3|0;%wSib z=1}yreF}PWL!;B+Sw~gaES=l1)K2LQ7Y3M?q`s@UroHhgJ-)@Kt5pIEU2eJSR>HOM z7RP!VUE}oySNVR)P zw_8ayFcS;ELYTCZhM~eba@bZa-h#^OUhtz&eA89CwU{*a4I-~)EtN}N;=VPuE%oq7 zy^N#1ELv1y%?NMxCB)1vc{*N+Y99qq*EUMZ7dFlH8{2NyY1E_pLBTc!@hj8Oeqmn# z)%?nLhgpgy69e8_(^w=Wvd&Jq?dts)DztsJ(lQsxQ|u8@5iFh{PNw}Yz5>=BHZ04g zBsEGi=(Eahs2~K(x9YBEki7L6s7Emggz?uI%vQ-jSg{J`4JLx`k5%dL3{52PA?&*g zYzHHY3b(+EwXfbZe`p@I2~ZQl>S~O0cZu~&18BKsVbj44 zQno1`p425Y2Y0CMTryszdmY0M8X?6kGrIhnjPdGP2zK#N-ecWKG9&=}x#lx|+^|qx z?K$zDqq}?blTg)f&*4^7oMx+}T1oZqqAZTyg52U!kJyWa_3W~UtZ8@#+U(guWm;n= zsKQp`O)ZR&2|?9+bHe(YhQc`re)d?PeJvw|fa~|n@^>sJTs|^q#Y$f4aZMX90He|* z_o6u!)ldEN@zo18H7z2u57r0Z)m zpZuw^_I()Wi@j?%#v1tdT3Sh^J!h`qsFJ<`Z|EEgK{??A_qAFLTl=6_gutV#e1mQr zVb4%L{L`X!-2doqv-Ku3f{clt$W>;-|3DQ@?=0O)KSHjNMz1y6+_SRUaeJazv-Jr7 zW)#`BrTb92QmYxKYD99TZ*FzpaaX=5GxNJOEgE|%c(4(?5T4`0{jEA6Ev*1L#M7^8 zh^FEMw9v>!uM_vqq6p6Z@75fO<8zHg7Z}Do|8cVU>XA4BnGlv4 z?#oy28O{WiJ>f)XzC1lu0;BV^Dds8mB@^bD^ABYkzUsT}ii-yc^H`N-n{o)LYo^*G zCqx_zWKLxQKT%q+f$G>ixmb!U@BN;FH12i$MKBoQKTD%3{d%K$!~J8vK!KRN>&!z{ sZ{^w4+ObN5qev$(6W77| Date: Thu, 12 Feb 2026 09:17:30 +0900 Subject: [PATCH 08/17] =?UTF-8?q?[REFACTOR]=20=EC=8B=A4=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=20=EB=B2=84=EC=8A=A4=20=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/BusDetail/BusRouteCell.swift | 2 +- .../Cell/DetailRouteBusCell.swift | 12 ++++++------ .../DetailRouteInfoBottomView.swift | 17 ++++++++++------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Atcha-iOS/Presentation/BusDetail/BusRouteCell.swift b/Atcha-iOS/Presentation/BusDetail/BusRouteCell.swift index 471b985..bce811f 100644 --- a/Atcha-iOS/Presentation/BusDetail/BusRouteCell.swift +++ b/Atcha-iOS/Presentation/BusDetail/BusRouteCell.swift @@ -222,7 +222,7 @@ class BusRouteCell: UICollectionViewCell { "\(updated.toHourMinuteSecondString) (\(remainText))", color: AtchaColor.Etc.remainTime ) - } else if updated > 0 { // 3분 이하 → "곧 도착" + } else if updated > 0 { // 2분 이하 → "곧 도착" let congestionText = congestion?.displayText let remainText: String if let congestionText, !congestionText.isEmpty { diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift index dc44030..8ceea8c 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift @@ -126,7 +126,7 @@ final class DetailRouteBusCell: UICollectionViewCell { withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel ) - var newAttributes = layoutAttributes + let newAttributes = layoutAttributes newAttributes.frame.size.height = ceil(size.height) return newAttributes } @@ -305,10 +305,10 @@ final class DetailRouteBusCell: UICollectionViewCell { } func setupBusRealTimeInfo(info: LegTrafficInfo?, busInfo: [RealTimeBusArrival]) { - if !isCurrentTimeBetween(startTime: info?.startTime, - endTime: info?.endTime) { - return - } +// if !isCurrentTimeBetween(startTime: info?.startTime, +// endTime: info?.endTime) { +// return +// } busTimerStackView.isHidden = false @@ -376,7 +376,7 @@ final class DetailRouteBusCell: UICollectionViewCell { return AtchaFont.B6_R_14("정보 없음", color: .gray) } - if remaining <= 90 { + if remaining <= 120 { return AtchaFont.B6_R_14("곧 도착", color: .widearea) } else { return AtchaFont.B6_R_14(formatSecondsToMinutesAndSeconds(remaining), color: .widearea) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift index 18ebce1..eef85a2 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift @@ -250,7 +250,7 @@ extension DetailRouteInfoBottomView { for: indexPath ) as! DetailRouteBusCell cell.didTapSummary = { [weak self] in -// self?.applySnapshot() + // self?.applySnapshot() self?.collectionView.collectionViewLayout.invalidateLayout() } cell.getNewBusRealTime = { [weak self] in @@ -280,11 +280,14 @@ extension DetailRouteInfoBottomView { } cell.configure(info: item.info) - // 이 부분에서 나눠서 값을 넣어줘야 해 - - // cell.setupBusRealTimeInfo(busInfo: self.busRealTimeInfo) - if let routeName = item.info?.route { - let matchedInfo = self.busRealTimeInfo.first(where: { $0.first?.routeName == routeName }) ?? [] + if let raw = item.info?.route { + let cellKey = raw.components(separatedBy: ":").last ?? raw + + let matchedInfo = self.busRealTimeInfo.first(where: { list in + guard let apiRaw = list.first?.routeName else { return false } + let apiKey = apiRaw.components(separatedBy: ":").last ?? apiRaw + return apiKey == cellKey + }) ?? [] cell.setupBusRealTimeInfo(info: item.info, busInfo: matchedInfo) } @@ -296,7 +299,7 @@ extension DetailRouteInfoBottomView { for: indexPath ) as! DetailRouteSubwayCell cell.didTapSummary = { [weak self] in -// self?.applySnapshot() + // self?.applySnapshot() self?.collectionView.collectionViewLayout.invalidateLayout() } cell.configure(info: item.info) From 26783567f5ef3aede6bda2ccb369bef225aa1c92 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Thu, 12 Feb 2026 10:25:09 +0900 Subject: [PATCH 09/17] =?UTF-8?q?[REFACTOR]=20=EC=8B=A4=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=20=EB=B2=84=EC=8A=A4=20=EC=8B=9C=EA=B0=84=20=EA=B9=9C=EB=B9=A1?= =?UTF-8?q?=EC=9E=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cell/DetailRouteBusCell.swift | 212 +++++++++--------- .../Cell/DetailRouteSubwayCell.swift | 111 ++++++++- .../DetailRouteInfoBottomView.swift | 39 +++- .../DetailRouteViewController.swift | 7 + .../DetailRoute/DetailRouteViewModel.swift | 74 +++++- 5 files changed, 317 insertions(+), 126 deletions(-) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift index 8ceea8c..5acb272 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift @@ -100,7 +100,6 @@ final class DetailRouteBusCell: UICollectionViewCell { animationView.isHidden = true animationView.stopAnimation() backgroundColor = .clear - busTimerStackView.isHidden = true isExpanded = false stationListStackView.isHidden = true @@ -304,96 +303,6 @@ final class DetailRouteBusCell: UICollectionViewCell { endLabel.attributedText = endCombinedLabel } - func setupBusRealTimeInfo(info: LegTrafficInfo?, busInfo: [RealTimeBusArrival]) { -// if !isCurrentTimeBetween(startTime: info?.startTime, -// endTime: info?.endTime) { -// return -// } - - busTimerStackView.isHidden = false - - countdownTimer?.invalidate() - reloadTimer?.invalidate() - - currentBusInfo = busInfo - - guard !busInfo.isEmpty else { - busTimerStackView.isHidden = false - busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("정보 없음", color: .gray300) - busTimerSecondLabel.text = "" - return - } - - currentBusInfo = currentBusInfo.filter { $0.remainingTime ?? 0 > 0 } - if currentBusInfo.isEmpty { - busTimerStackView.isHidden = false - busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("도착 또는 출발", color: .widearea) - return - } - - busTimerStackView.isHidden = false - updateBusTimerLabels() - - // 1초마다 시간 감소 - countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in - self?.decrementRemainingTime() - } - - // 30초마다 재요청 - reloadTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: true) { [weak self] _ in - self?.getNewBusRealTime?() - } - } - - private func decrementRemainingTime() { - for i in 0.. 0 else { continue } - currentBusInfo[i].remainingTime = time - 1 - } - - currentBusInfo = currentBusInfo.filter { - if let time = $0.remainingTime { - return time > 0 - } - return false - } - - if currentBusInfo.isEmpty { - busTimerStackView.isHidden = true - countdownTimer?.invalidate() - } else { - updateBusTimerLabels() - } - } - - private func updateBusTimerLabels() { - func labelText(for info: RealTimeBusArrival) -> NSAttributedString { - if info.busStatus == .end { - return AtchaFont.B6_R_14("운행 종료", color: .gray) - } - - guard let remaining = info.remainingTime else { - return AtchaFont.B6_R_14("정보 없음", color: .gray) - } - - if remaining <= 120 { - return AtchaFont.B6_R_14("곧 도착", color: .widearea) - } else { - return AtchaFont.B6_R_14(formatSecondsToMinutesAndSeconds(remaining), color: .widearea) - } - } - - switch currentBusInfo.count { - case 2: - busTimerFirstLabel.attributedText = labelText(for: currentBusInfo[0]) - busTimerSecondLabel.attributedText = labelText(for: currentBusInfo[1]) - case 1: - busTimerFirstLabel.attributedText = labelText(for: currentBusInfo[0]) - busTimerSecondLabel.text = "" - default: - busTimerStackView.isHidden = true - } - } private func isCurrentTimeBetween(startTime: String?, endTime: String?) -> Bool { guard let startTime, let endTime else { return false } @@ -439,15 +348,6 @@ final class DetailRouteBusCell: UICollectionViewCell { animationView.startAnimationIfNeeded(forceRestart: true) backgroundColor = UIColor.opacity100 } - - private func formatSecondsToMinutesAndSeconds(_ seconds: Int?) -> String { - guard let seconds = seconds else { return "시간 없음" } - - let minutes = seconds / 60 - let remainingSeconds = seconds % 60 - - return "\(minutes)분 \(remainingSeconds)초" - } } // MARK: Action @@ -510,3 +410,115 @@ extension DetailRouteBusCell { } } } + +// MARK: - Bus Realtime + +extension DetailRouteBusCell { + func setupBusRealTimeInfo(info: LegTrafficInfo?, busInfo: [RealTimeBusArrival]) { + busTimerStackView.isHidden = false + + let filtered = busInfo + .filter { ($0.remainingTime ?? -1) > 0 } + + currentBusInfo = filtered + + guard !busInfo.isEmpty else { + stopCountdownTimer() + busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("정보 없음", color: .gray300) + busTimerSecondLabel.text = "" + return + } + + guard !currentBusInfo.isEmpty else { + stopCountdownTimer() + busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("도착 또는 출발", color: .widearea) + busTimerSecondLabel.text = "" + return + } + + updateBusTimerLabels() + startCountdownTimerIfNeeded() + } + + private func startCountdownTimerIfNeeded() { + // 이미 돌고 있으면 유지 + if countdownTimer != nil { return } + + countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + self?.decrementRemainingTime() + } + + // 스크롤 중에도 잘 돌게 common mode 추천 + if let timer = countdownTimer { + RunLoop.main.add(timer, forMode: .common) + } + } + + private func stopCountdownTimer() { + countdownTimer?.invalidate() + countdownTimer = nil + } + + // 1초마다 감소 + private func decrementRemainingTime() { + guard !currentBusInfo.isEmpty else { + stopCountdownTimer() + return + } + + for i in 0.. 0 } + + if currentBusInfo.isEmpty { + stopCountdownTimer() + busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("도착 또는 출발", color: .widearea) + busTimerSecondLabel.text = "" + return + } + + updateBusTimerLabels() + } + + private func updateBusTimerLabels() { + + func labelText(for info: RealTimeBusArrival) -> NSAttributedString { + if info.busStatus == .end { + return AtchaFont.B6_R_14("운행 종료", color: .gray) + } + + guard let remaining = info.remainingTime else { + return AtchaFont.B6_R_14("정보 없음", color: .gray300) + } + + if remaining <= 120 { + return AtchaFont.B6_R_14("곧 도착", color: .widearea) + } else { + return AtchaFont.B6_R_14(formatSecondsToMinutesAndSeconds(remaining), color: .widearea) + } + } + + switch currentBusInfo.count { + case 2: + busTimerFirstLabel.attributedText = labelText(for: currentBusInfo[0]) + busTimerSecondLabel.attributedText = labelText(for: currentBusInfo[1]) + case 1: + busTimerFirstLabel.attributedText = labelText(for: currentBusInfo[0]) + busTimerSecondLabel.text = "" + default: + // 3개 이상이면 우선 2개만 보여주거나, 숨기지 말고 2개만 보여주자 + busTimerFirstLabel.attributedText = labelText(for: currentBusInfo[0]) + busTimerSecondLabel.attributedText = labelText(for: currentBusInfo[1]) + } + } + + private func formatSecondsToMinutesAndSeconds(_ seconds: Int?) -> String { + guard let seconds = seconds, seconds >= 0 else { return "시간 없음" } + let minutes = seconds / 60 + let remainingSeconds = seconds % 60 + return "\(minutes)분 \(remainingSeconds)초" + } +} diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift index d883226..b64ed04 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift @@ -57,6 +57,19 @@ final class DetailRouteSubwayCell: UICollectionViewCell { private var stationListStackViewBottomConstraint: Constraint? private var endLabelTopConstraintWithoutStack: Constraint? + private let subwayDirectionLabel = UILabel() + private let subwayTimerLabel = UILabel() + private lazy var subwayRealtimeStackView: UIStackView = { + let stack = UIStackView(arrangedSubviews: [subwayDirectionLabel, subwayTimerLabel]) + stack.axis = .horizontal + stack.alignment = .center + stack.spacing = 8 + return stack + }() + + private var subwayCountdownTimer: Timer? + private var currentRemainingSec: Int? + override init(frame: CGRect) { super.init(frame: frame) setupUI() @@ -83,6 +96,12 @@ final class DetailRouteSubwayCell: UICollectionViewCell { stationListStackViewTopConstraint?.isActive = false stationListStackViewBottomConstraint?.isActive = false endLabelTopConstraintWithoutStack?.isActive = true + + subwayDirectionLabel.text = nil + subwayTimerLabel.text = nil + subwayCountdownTimer?.invalidate() + subwayCountdownTimer = nil + currentRemainingSec = nil } override func preferredLayoutAttributesFitting( @@ -100,7 +119,7 @@ final class DetailRouteSubwayCell: UICollectionViewCell { withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel ) - var newAttributes = layoutAttributes + let newAttributes = layoutAttributes newAttributes.frame.size.height = ceil(size.height) return newAttributes } @@ -117,7 +136,7 @@ final class DetailRouteSubwayCell: UICollectionViewCell { stickContainerView, startStackView, endStackView, - subwayBadgeLabel, + subwayRealtimeStackView, summaryView, stationListStackView) @@ -134,6 +153,10 @@ final class DetailRouteSubwayCell: UICollectionViewCell { stationListStackView.axis = .vertical stationListStackView.spacing = 10 stationListStackView.isHidden = true + + subwayDirectionLabel.numberOfLines = 1 + subwayTimerLabel.numberOfLines = 1 + } private func setupInitialConstraintState() { @@ -185,14 +208,19 @@ final class DetailRouteSubwayCell: UICollectionViewCell { } private func setupInfoConstrains() { - subwayBadgeLabel.snp.makeConstraints { make in + // subwayBadgeLabel.snp.makeConstraints { make in + // make.leading.equalTo(startLabel.snp.leading) + // make.top.equalTo(startStackView.snp.bottom).offset(8) + // } + + subwayRealtimeStackView.snp.makeConstraints { make in make.leading.equalTo(startLabel.snp.leading) make.top.equalTo(startStackView.snp.bottom).offset(8) } summaryView.snp.makeConstraints { make in make.leading.equalTo(startLabel.snp.leading) - make.top.equalTo(subwayBadgeLabel.snp.bottom).offset(16) + make.top.equalTo(subwayRealtimeStackView.snp.bottom).offset(16) } stationListStackView.snp.makeConstraints { @@ -330,6 +358,81 @@ final class DetailRouteSubwayCell: UICollectionViewCell { return todayNow >= todayStart && todayNow < todayEnd } } + + func setupSubwayRealTime(routeName: String?, infos: [SubwayRealTimeInfo]) { + subwayCountdownTimer?.invalidate() + subwayCountdownTimer = nil + currentRemainingSec = nil + + subwayTimerLabel.isHidden = false + subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("", color: .white) + + guard let routeName, !routeName.isEmpty else { return } + + let key = routeName.components(separatedBy: ":").last ?? routeName + + let matched = infos.first { info in + let apiRaw = info.routeName ?? "" + let apiKey = apiRaw.components(separatedBy: ":").last ?? apiRaw + return apiKey == key + } + + let destination = matched?.destination ?? "" + subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("\(destination)행", color: .white) + + guard let sec = matched?.remainingTime, sec >= 0 else { + return + } + + currentRemainingSec = sec + updateSubwayTimerLabel() + startSubwayCountdownTimer() + } + + private func startSubwayCountdownTimer() { + subwayCountdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + guard let self else { return } + guard let sec = self.currentRemainingSec else { return } + + self.currentRemainingSec = sec - 1 + self.updateSubwayTimerLabel() + } + } + + private func updateSubwayTimerLabel() { + guard let sec = currentRemainingSec else { + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("불러오는 중", color: .widearea) + return + } + + if sec < 0 { + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("불러오는 중", color: .widearea) + return + } + + if sec == 0 { + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("도착 또는 출발", color: .widearea) + subwayCountdownTimer?.invalidate() + subwayCountdownTimer = nil + return + } + + // 2분 이하(<=120초)면 "곧 도착" + if sec <= 120 { + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("곧 도착", color: .widearea) + return + } + + // 그 외는 mm:ss + subwayTimerLabel.attributedText = AtchaFont.B6_R_14(formatMinSecKorean(sec), color: .widearea) + } + + + private func formatMinSecKorean(_ seconds: Int) -> String { + let m = seconds / 60 + let s = seconds % 60 + return "\(m)분 \(s)초" + } } // MARK: Action diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift index eef85a2..2a79e57 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift @@ -40,6 +40,7 @@ final class DetailRouteInfoBottomView: UIView { private let handleView: UIView = UIView() private var startAddress: String = "" private var busRealTimeInfo: [[RealTimeBusArrival]] = [] + private var subwayRealTimeInfo: [SubwayRealTimeInfo] = [] var onBusDetail: ((BusDetailInfo) -> Void)? var getNewBusRealTime: (() -> Void)? @@ -133,6 +134,28 @@ final class DetailRouteInfoBottomView: UIView { // v2 func setupBusTimerLabel(_ time: [[RealTimeBusArrival]]) { busRealTimeInfo = time + + for cell in collectionView.visibleCells { + guard let busCell = cell as? DetailRouteBusCell else { continue } + + // 현재 셀이 어떤 route였는지 알아야 다시 매칭 가능 + guard let route = busCell.currentLegTrafficInfo?.route else { continue } + let cellKey = route.components(separatedBy: ":").last ?? route + + let matchedInfo = busRealTimeInfo.first(where: { list in + guard let apiRaw = list.first?.routeName else { return false } + let apiKey = apiRaw.components(separatedBy: ":").last ?? apiRaw + return apiKey == cellKey + }) ?? [] + + + guard !matchedInfo.isEmpty else { continue } + busCell.setupBusRealTimeInfo(info: busCell.currentLegTrafficInfo, busInfo: matchedInfo) + } + } + + func setupSubwayRealTime(_ infos: [SubwayRealTimeInfo]) { + subwayRealTimeInfo = infos collectionView.reloadData() } @@ -280,17 +303,6 @@ extension DetailRouteInfoBottomView { } cell.configure(info: item.info) - if let raw = item.info?.route { - let cellKey = raw.components(separatedBy: ":").last ?? raw - - let matchedInfo = self.busRealTimeInfo.first(where: { list in - guard let apiRaw = list.first?.routeName else { return false } - let apiKey = apiRaw.components(separatedBy: ":").last ?? apiRaw - return apiKey == cellKey - }) ?? [] - cell.setupBusRealTimeInfo(info: item.info, busInfo: matchedInfo) - } - return cell case .subway: @@ -303,6 +315,11 @@ extension DetailRouteInfoBottomView { self?.collectionView.collectionViewLayout.invalidateLayout() } cell.configure(info: item.info) + + if let routeName = item.info?.route { + let matched = self.subwayRealTimeInfo.filter { $0.routeName == routeName } + cell.setupSubwayRealTime(routeName: routeName, infos: matched) + } return cell default: diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift index 6673292..75348ac 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift @@ -175,6 +175,13 @@ final class DetailRouteViewController: BaseViewController, } .store(in: &cancellables) + viewModel.$subwayRealTimeInfos + .receive(on: RunLoop.main) + .sink { [weak self] infos in + self?.bottomSheet.setupSubwayRealTime(infos) + } + .store(in: &cancellables) + viewModel.$address .receive(on: RunLoop.main) .compactMap { $0 } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift index 3754068..e71ed7a 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift @@ -34,10 +34,18 @@ final class DetailRouteViewModel: BaseViewModel { // @Published var busRealTimeInfo: [RealTimeBusArrival] = [] @Published var busRealTimeInfos: [[RealTimeBusArrival]] = [] + private var busRoutes: [String] = [] + private var busRealTimeMap: [String: [RealTimeBusArrival]] = [:] + private var busPollingTask: Task? + @Published var subwayRealTimeInfos: [SubwayRealTimeInfo] = [] @Published private(set) var context: DetailRouteContext + deinit { + stopBusPolling() + } + init(address: String, infos: LegInfo, context: DetailRouteContext, @@ -63,18 +71,25 @@ final class DetailRouteViewModel: BaseViewModel { func fetchInfo() { legtPathInfo = infos.pathInfo legTrafficInfo = infos.trafficInfo - + + guard context == .afterReigster else { return } // 버스 - let busDetailInfo = infos.busInfo.filter { $0.routeName?.isEmpty == false } + let routes = infos.busInfo + .compactMap { $0.routeName } + .filter { !$0.isEmpty && $0.contains(":") } + + busRoutes = Array(Set(routes)) // 중복 제거 + busRealTimeMap.removeAll() busRealTimeInfos = [] - busDetailInfo.forEach { info in - if let routeName = info.routeName, routeName.contains(":") { - Task { await getBusRealTimeInfo(request: routeName) } - } + + // 최초 1회 로드 + Task { [weak self] in + await self?.refreshAllBusRealTime() } - guard context == .afterReigster else { return } - + // 15초 폴링 시작 + startBusPolling() + subwayRealTimeInfos = [] let subwayRoutes = Array(Set( infos.trafficInfo @@ -82,7 +97,7 @@ final class DetailRouteViewModel: BaseViewModel { .compactMap { $0.route } .filter { !$0.isEmpty } )) - + subwayRoutes.forEach { route in Task { await getSubwayRealTimeInfo(routeName: route) } } @@ -95,7 +110,6 @@ final class DetailRouteViewModel: BaseViewModel { let response = try await busInfoUseCase.getBusRealTimeInfo(request) busRealTimeInfos.append(response) // busRealTimeInfo = response - print("실시간 버스 조회 성공요! : \(busRealTimeInfos)") } catch { print("실시간 버스 조회 실패요!") } @@ -108,7 +122,6 @@ final class DetailRouteViewModel: BaseViewModel { let infos = try await subwayInfoUseCase.subwayRealTimeInfo(.init(routeName: routeName)) subwayRealTimeInfos.removeAll { $0.routeName == routeName } // 기존 제거 subwayRealTimeInfos.append(contentsOf: infos) - print("지하철 정보:\(infos)") } catch { print("실시간 지하철 조회 실패: \(error)") } @@ -151,4 +164,43 @@ final class DetailRouteViewModel: BaseViewModel { } } } + + private func startBusPolling() { + stopBusPolling() + + busPollingTask = Task { [weak self] in + guard let self else { return } + + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 15_000_000_000) + if Task.isCancelled { break } + await self.refreshAllBusRealTime() + } + } + } + + private func stopBusPolling() { + busPollingTask?.cancel() + busPollingTask = nil + } + + @MainActor + private func refreshAllBusRealTime() async { + guard !busRoutes.isEmpty else { return } + + // 병렬로 받아오고 싶으면 TaskGroup, 단순이면 for-await도 OK + for route in busRoutes { + do { + let response = try await busInfoUseCase.getBusRealTimeInfo(route) + busRealTimeMap[route] = response + } catch { + // 실패했을 때 기존 값 유지(중요: 여기서 map 지우면 UI가 "꺼짐") + print("실시간 버스 조회 실패: \(route), \(error)") + } + } + + // UI용 배열 갱신 (순서가 중요하면 busRoutes 순서대로) + busRealTimeInfos = busRoutes.compactMap { busRealTimeMap[$0] } + } + } From 1f63b5d524640be1ea5fde588d7d61a6f33edf3a Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Thu, 12 Feb 2026 10:46:15 +0900 Subject: [PATCH 10/17] =?UTF-8?q?[REFACTOR]=20=EC=8B=A4=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=20=EC=A7=80=ED=95=98=EC=B2=A0=20=EB=8F=84=EC=B0=A9=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EA=B9=9C=EB=B9=A1=EC=9E=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cell/DetailRouteSubwayCell.swift | 137 +++++++++++------- .../DetailRouteInfoBottomView.swift | 36 ++++- .../DetailRouteViewController.swift | 2 +- .../DetailRoute/DetailRouteViewModel.swift | 56 ++++++- 4 files changed, 166 insertions(+), 65 deletions(-) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift index b64ed04..305a3a2 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift @@ -70,6 +70,9 @@ final class DetailRouteSubwayCell: UICollectionViewCell { private var subwayCountdownTimer: Timer? private var currentRemainingSec: Int? + var currentLegTrafficInfo: LegTrafficInfo? = nil + + override init(frame: CGRect) { super.init(frame: frame) setupUI() @@ -268,6 +271,8 @@ final class DetailRouteSubwayCell: UICollectionViewCell { } func configure(info: LegTrafficInfo?) { + currentLegTrafficInfo = info + stationInfos = [] guard let info = info, let passStopList = info.passStopList, @@ -358,16 +363,52 @@ final class DetailRouteSubwayCell: UICollectionViewCell { return todayNow >= todayStart && todayNow < todayEnd } } +} + +// MARK: Action +extension DetailRouteSubwayCell { + private func setupAction() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSummaryButton)) + summaryView.addGestureRecognizer(tapGesture) + } + @objc private func handleSummaryButton() { + isExpanded.toggle() + + if isExpanded { + endLabelTopConstraintWithoutStack?.isActive = false + stationListStackViewTopConstraint?.isActive = true + stationListStackViewBottomConstraint?.isActive = true + } else { + stationListStackViewTopConstraint?.isActive = false + stationListStackViewBottomConstraint?.isActive = false + endLabelTopConstraintWithoutStack?.isActive = true + } + + stationListStackView.isHidden = !isExpanded + stationListStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + addStationNameLabel(info: stationInfos) + + contentView.setNeedsLayout() + contentView.layoutIfNeeded() + + didTapSummary?() + } +} + +extension DetailRouteSubwayCell { + func setupSubwayRealTime(routeName: String?, infos: [SubwayRealTimeInfo]) { - subwayCountdownTimer?.invalidate() - subwayCountdownTimer = nil + stopSubwayCountdownTimer() currentRemainingSec = nil subwayTimerLabel.isHidden = false subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("", color: .white) - guard let routeName, !routeName.isEmpty else { return } + guard let routeName, !routeName.isEmpty else { + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("정보 없음", color: .gray300) + return + } let key = routeName.components(separatedBy: ":").last ?? routeName @@ -377,91 +418,83 @@ final class DetailRouteSubwayCell: UICollectionViewCell { return apiKey == key } - let destination = matched?.destination ?? "" + guard let matched else { + subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("정보 없음", color: .gray300) + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("정보 없음", color: .gray300) + return + } + + let destination = matched.destination ?? "" subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("\(destination)행", color: .white) - guard let sec = matched?.remainingTime, sec >= 0 else { + guard let sec = matched.remainingTime, sec >= 0 else { + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("불러오는 중", color: .widearea) return } currentRemainingSec = sec updateSubwayTimerLabel() - startSubwayCountdownTimer() + startSubwayCountdownTimerIfNeeded() } - - private func startSubwayCountdownTimer() { + + private func startSubwayCountdownTimerIfNeeded() { + if subwayCountdownTimer != nil { return } + subwayCountdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in - guard let self else { return } - guard let sec = self.currentRemainingSec else { return } + self?.decrementSubwayRemainingTime() + } - self.currentRemainingSec = sec - 1 - self.updateSubwayTimerLabel() + if let timer = subwayCountdownTimer { + RunLoop.main.add(timer, forMode: .common) } } - private func updateSubwayTimerLabel() { + private func stopSubwayCountdownTimer() { + subwayCountdownTimer?.invalidate() + subwayCountdownTimer = nil + } + + private func decrementSubwayRemainingTime() { guard let sec = currentRemainingSec else { - subwayTimerLabel.attributedText = AtchaFont.B6_R_14("불러오는 중", color: .widearea) + stopSubwayCountdownTimer() return } - if sec < 0 { + let next = sec - 1 + currentRemainingSec = next + + if next <= 0 { + currentRemainingSec = 0 + updateSubwayTimerLabel() + stopSubwayCountdownTimer() + return + } + + updateSubwayTimerLabel() + } + + private func updateSubwayTimerLabel() { + guard let sec = currentRemainingSec else { subwayTimerLabel.attributedText = AtchaFont.B6_R_14("불러오는 중", color: .widearea) return } if sec == 0 { subwayTimerLabel.attributedText = AtchaFont.B6_R_14("도착 또는 출발", color: .widearea) - subwayCountdownTimer?.invalidate() - subwayCountdownTimer = nil return } - // 2분 이하(<=120초)면 "곧 도착" if sec <= 120 { subwayTimerLabel.attributedText = AtchaFont.B6_R_14("곧 도착", color: .widearea) return } - // 그 외는 mm:ss subwayTimerLabel.attributedText = AtchaFont.B6_R_14(formatMinSecKorean(sec), color: .widearea) } - - + private func formatMinSecKorean(_ seconds: Int) -> String { let m = seconds / 60 let s = seconds % 60 return "\(m)분 \(s)초" } } - -// MARK: Action -extension DetailRouteSubwayCell { - private func setupAction() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSummaryButton)) - summaryView.addGestureRecognizer(tapGesture) - } - - @objc private func handleSummaryButton() { - isExpanded.toggle() - - if isExpanded { - endLabelTopConstraintWithoutStack?.isActive = false - stationListStackViewTopConstraint?.isActive = true - stationListStackViewBottomConstraint?.isActive = true - } else { - stationListStackViewTopConstraint?.isActive = false - stationListStackViewBottomConstraint?.isActive = false - endLabelTopConstraintWithoutStack?.isActive = true - } - - stationListStackView.isHidden = !isExpanded - stationListStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - addStationNameLabel(info: stationInfos) - - contentView.setNeedsLayout() - contentView.layoutIfNeeded() - - didTapSummary?() - } -} diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift index 2a79e57..9a08a76 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift @@ -154,9 +154,16 @@ final class DetailRouteInfoBottomView: UIView { } } - func setupSubwayRealTime(_ infos: [SubwayRealTimeInfo]) { + func setupSubwayTimerLabel(_ infos: [SubwayRealTimeInfo]) { subwayRealTimeInfo = infos - collectionView.reloadData() + + for cell in collectionView.visibleCells { + guard let subwayCell = cell as? DetailRouteSubwayCell else { continue } + guard let route = subwayCell.currentLegTrafficInfo?.route, !route.isEmpty else { continue } + + let matched = subwayRealTimeInfo.filter { $0.routeName == route } + subwayCell.setupSubwayRealTime(routeName: route, infos: matched) + } } func setupStartAddress(_ address: String) { @@ -303,6 +310,18 @@ extension DetailRouteInfoBottomView { } cell.configure(info: item.info) + if let route = item.info?.route, !route.isEmpty { + let cellKey = route.components(separatedBy: ":").last ?? route + + let matchedInfo = self.busRealTimeInfo.first(where: { list in + guard let apiRaw = list.first?.routeName else { return false } + let apiKey = apiRaw.components(separatedBy: ":").last ?? apiRaw + return apiKey == cellKey + }) ?? [] + + cell.setupBusRealTimeInfo(info: item.info, busInfo: matchedInfo) + } + return cell case .subway: @@ -316,10 +335,15 @@ extension DetailRouteInfoBottomView { } cell.configure(info: item.info) - if let routeName = item.info?.route { - let matched = self.subwayRealTimeInfo.filter { $0.routeName == routeName } - cell.setupSubwayRealTime(routeName: routeName, infos: matched) - } + if let route = item.info?.route, !route.isEmpty { + let key = route.components(separatedBy: ":").last ?? route + let matched = self.subwayRealTimeInfo.filter { info in + let apiRaw = info.routeName ?? "" + let apiKey = apiRaw.components(separatedBy: ":").last ?? apiRaw + return apiKey == key + } + cell.setupSubwayRealTime(routeName: route, infos: matched) + } return cell default: diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift index 75348ac..bb359ad 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift @@ -178,7 +178,7 @@ final class DetailRouteViewController: BaseViewController, viewModel.$subwayRealTimeInfos .receive(on: RunLoop.main) .sink { [weak self] infos in - self?.bottomSheet.setupSubwayRealTime(infos) + self?.bottomSheet.setupSubwayTimerLabel(infos) } .store(in: &cancellables) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift index e71ed7a..8d98096 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift @@ -39,11 +39,14 @@ final class DetailRouteViewModel: BaseViewModel { private var busPollingTask: Task? @Published var subwayRealTimeInfos: [SubwayRealTimeInfo] = [] + private var subwayRoutes: [String] = [] + private var subwayPollingTask: Task? @Published private(set) var context: DetailRouteContext deinit { stopBusPolling() + stopSubwayPolling() } init(address: String, @@ -90,7 +93,6 @@ final class DetailRouteViewModel: BaseViewModel { // 15초 폴링 시작 startBusPolling() - subwayRealTimeInfos = [] let subwayRoutes = Array(Set( infos.trafficInfo .filter { $0.mode == .subway } @@ -98,9 +100,15 @@ final class DetailRouteViewModel: BaseViewModel { .filter { !$0.isEmpty } )) - subwayRoutes.forEach { route in - Task { await getSubwayRealTimeInfo(routeName: route) } + self.subwayRoutes = subwayRoutes + // 여기서 removeAll 하면 첫 표시가 비었다가 생길 수 있음. + // "최초 진입 때만 비우고", 폴링에서는 기존 유지가 더 안정적. + self.subwayRealTimeInfos = [] + + Task { [weak self] in + await self?.refreshAllSubwayRealTime() } + startSubwayPolling() } @MainActor @@ -188,19 +196,55 @@ final class DetailRouteViewModel: BaseViewModel { private func refreshAllBusRealTime() async { guard !busRoutes.isEmpty else { return } - // 병렬로 받아오고 싶으면 TaskGroup, 단순이면 for-await도 OK for route in busRoutes { do { let response = try await busInfoUseCase.getBusRealTimeInfo(route) busRealTimeMap[route] = response } catch { - // 실패했을 때 기존 값 유지(중요: 여기서 map 지우면 UI가 "꺼짐") + // 실패 시 기존 값 유지 print("실시간 버스 조회 실패: \(route), \(error)") } } - // UI용 배열 갱신 (순서가 중요하면 busRoutes 순서대로) busRealTimeInfos = busRoutes.compactMap { busRealTimeMap[$0] } } + // MARK: - Subway Polling + + private func startSubwayPolling() { + stopSubwayPolling() + + subwayPollingTask = Task { [weak self] in + guard let self else { return } + + while !Task.isCancelled { + try? await Task.sleep(nanoseconds: 15_000_000_000) + if Task.isCancelled { break } + await self.refreshAllSubwayRealTime() + } + } + } + + private func stopSubwayPolling() { + subwayPollingTask?.cancel() + subwayPollingTask = nil + } + + @MainActor + private func refreshAllSubwayRealTime() async { + guard !subwayRoutes.isEmpty else { return } + + for route in subwayRoutes { + do { + let infos = try await subwayInfoUseCase.subwayRealTimeInfo(.init(routeName: route)) + + // 성공한 route만 교체 (실패하면 기존 유지) + subwayRealTimeInfos.removeAll { $0.routeName == route } + subwayRealTimeInfos.append(contentsOf: infos) + } catch { + // 실패 시 기존 유지 + print("실시간 지하철 조회 실패: \(route), \(error)") + } + } + } } From ab193b6d3ebfbc35b1a0592349d7cb74c0f3e0f1 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Thu, 12 Feb 2026 11:19:07 +0900 Subject: [PATCH 11/17] =?UTF-8?q?[REFACTOR]=20=EC=83=81=EC=84=B8=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cell/DetailRouteBusCell.swift | 13 ++- .../Cell/DetailRouteSubwayCell.swift | 19 +++- .../DetailRouteInfoBottomView.swift | 22 ++++ .../DetailRouteViewController.swift | 100 ++++++++++++++++-- .../DetailRoute/DetailRouteViewModel.swift | 34 ++++++ 5 files changed, 172 insertions(+), 16 deletions(-) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift index 5acb272..7d552a7 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift @@ -80,6 +80,8 @@ final class DetailRouteBusCell: UICollectionViewCell { var currentLegTrafficInfo: LegTrafficInfo? = nil var currentBusInfo: [RealTimeBusArrival] = [] + private var isArrivedEffectOn = false + override init(frame: CGRect) { super.init(frame: frame) setupUI() @@ -343,11 +345,20 @@ final class DetailRouteBusCell: UICollectionViewCell { } func isNowUserLocationArrived() { - // 해당시간에 들어와야 애니메이션 실행 합니다. + if isArrivedEffectOn { return } + isArrivedEffectOn = true animationView.isHidden = false animationView.startAnimationIfNeeded(forceRestart: true) backgroundColor = UIColor.opacity100 } + + func stopArrivedEffectIfNeeded() { + guard isArrivedEffectOn else { return } + isArrivedEffectOn = false + animationView.stopAnimation() + animationView.isHidden = true + backgroundColor = .clear + } } // MARK: Action diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift index 305a3a2..555450c 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift @@ -71,7 +71,7 @@ final class DetailRouteSubwayCell: UICollectionViewCell { private var currentRemainingSec: Int? var currentLegTrafficInfo: LegTrafficInfo? = nil - + private var isArrivedEffectOn = false override init(frame: CGRect) { super.init(frame: frame) @@ -304,9 +304,9 @@ final class DetailRouteSubwayCell: UICollectionViewCell { endCombinedLabel.append(AtchaFont.B3_M_15(" 하차", color: .gray500)) endLabel.attributedText = endCombinedLabel - if isCurrentTimeBetween(startTime: info.startTime, endTime: info.endTime) { - isNowUserLocationArrived() - } +// if isCurrentTimeBetween(startTime: info.startTime, endTime: info.endTime) { +// isNowUserLocationArrived() +// } } private func addStationNameLabel(info: [PassStopList]) { @@ -320,11 +320,20 @@ final class DetailRouteSubwayCell: UICollectionViewCell { } func isNowUserLocationArrived() { - // 해당시간에 들어와야 애니메이션 실행 합니다. + if isArrivedEffectOn { return } + isArrivedEffectOn = true animationView.isHidden = false animationView.startAnimationIfNeeded(forceRestart: true) backgroundColor = UIColor.opacity100 } + + func stopArrivedEffectIfNeeded() { + guard isArrivedEffectOn else { return } + isArrivedEffectOn = false + animationView.stopAnimation() + animationView.isHidden = true + backgroundColor = .clear + } private func isCurrentTimeBetween(startTime: String?, endTime: String?) -> Bool { guard let startTime, let endTime else { return false } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift index 9a08a76..33b1ff0 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift @@ -352,6 +352,28 @@ extension DetailRouteInfoBottomView { } } } + + func updateProximityHighlight(nearLegIDs: Set) { + for cell in collectionView.visibleCells { + if let busCell = cell as? DetailRouteBusCell, + let leg = busCell.currentLegTrafficInfo { + if nearLegIDs.contains(leg.id) { + busCell.isNowUserLocationArrived() + } else { + busCell.stopArrivedEffectIfNeeded() + } + } + + if let subwayCell = cell as? DetailRouteSubwayCell, + let leg = subwayCell.currentLegTrafficInfo { + if nearLegIDs.contains(leg.id) { + subwayCell.isNowUserLocationArrived() + } else { + subwayCell.stopArrivedEffectIfNeeded() + } + } + } + } } // MARK: - Gesture diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift index bb359ad..a0d27a1 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift @@ -9,6 +9,7 @@ import UIKit import CoreLocation import TMapSDK import VSMSDK +import MapKit final class DetailRouteViewController: BaseViewController, TMapWrapperDelegate { @@ -31,6 +32,12 @@ final class DetailRouteViewController: BaseViewController, setupUI() setupAutoLayout() bindView() +// +//#if DEBUG +//// ✅ 용산구청(대략) - 필요하면 조금씩 조절 +//viewModel.mockLocation = CLLocationCoordinate2D(latitude: 37.5326, longitude: 126.9909) +//viewModel.currentLocation = viewModel.mockLocation +//#endif } override func viewDidLayoutSubviews() { @@ -188,19 +195,55 @@ final class DetailRouteViewController: BaseViewController, .sink { [weak self] address in self?.bottomSheet.setupStartAddress(address) } .store(in: &cancellables) +// viewModel.$currentLocation +// .compactMap { $0 } +// .receive(on: DispatchQueue.main) +// .sink { [weak self] location in +// guard let self else { return } +// mapContainerView.updateUserMarker(location: location) +// let offsetLatitude = location.latitude - 0.0003 +// let offsetLocation = CLLocationCoordinate2D( +// latitude: offsetLatitude, +// longitude: location.longitude +// ) +// +// mapContainerView.setupZoomCenter(location: offsetLocation) +// } +// .store(in: &cancellables) viewModel.$currentLocation .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [weak self] location in + .receive(on: DispatchQueue.global(qos: .userInitiated)) + .sink { [weak self] loc in guard let self else { return } - mapContainerView.updateUserMarker(location: location) - let offsetLatitude = location.latitude - 0.0003 - let offsetLocation = CLLocationCoordinate2D( - latitude: offsetLatitude, - longitude: location.longitude - ) - - mapContainerView.setupZoomCenter(location: offsetLocation) + + let threshold: CLLocationDistance = 300.0 + + // trafficInfo와 pathInfo를 같은 leg 순서로 zip 한다는 가정 + let pairs = zip(self.viewModel.legTrafficInfo, self.viewModel.legtPathInfo) + + var near: Set = [] + + for (traffic, path) in pairs { + guard traffic.mode == path.mode else { continue } + guard let shape = path.passShape, !shape.isEmpty else { continue } + + let coords = self.convertShapeToCoords(shape) + let d = self.distanceToPolylineMeters(point: loc, polyline: coords) + if d <= threshold { + near.insert(traffic.id) + } + } + + DispatchQueue.main.async { + self.viewModel.nearLegIDs = near + } + } + .store(in: &cancellables) + + viewModel.$nearLegIDs + .receive(on: RunLoop.main) + .sink { [weak self] near in + self?.bottomSheet.updateProximityHighlight(nearLegIDs: near) } .store(in: &cancellables) @@ -423,3 +466,40 @@ extension DetailRouteViewController { present(popupVC, animated: false) } } + +extension DetailRouteViewController { + private func distanceToPolylineMeters( + point: CLLocationCoordinate2D, + polyline: [CLLocationCoordinate2D] + ) -> CLLocationDistance { + guard polyline.count >= 2 else { return .greatestFiniteMagnitude } + + let p = MKMapPoint(point) + var best = CLLocationDistance.greatestFiniteMagnitude + + for i in 0..<(polyline.count - 1) { + let a = MKMapPoint(polyline[i]) + let b = MKMapPoint(polyline[i + 1]) + + let abx = b.x - a.x + let aby = b.y - a.y + let apx = p.x - a.x + let apy = p.y - a.y + + let ab2 = abx*abx + aby*aby + if ab2 == 0 { // 같은 점이면 점-점 거리 + best = min(best, p.distance(to: a)) + continue + } + + // 투영 비율 t를 0~1로 clamp + var t = (apx*abx + apy*aby) / ab2 + t = max(0, min(1, t)) + + let closest = MKMapPoint(x: a.x + t*abx, y: a.y + t*aby) + best = min(best, p.distance(to: closest)) + } + + return best + } +} diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift index 8d98096..1dd5937 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift @@ -43,6 +43,12 @@ final class DetailRouteViewModel: BaseViewModel { private var subwayPollingTask: Task? @Published private(set) var context: DetailRouteContext + @Published var nearLegIDs: Set = [] + +// +//#if DEBUG +//@Published var mockLocation: CLLocationCoordinate2D? = nil +//#endif deinit { stopBusPolling() @@ -151,6 +157,34 @@ final class DetailRouteViewModel: BaseViewModel { } } } +// func requestPermissionAndStartTracking() { +// Task { +// let status = await authorizationUseCase.askLocationPermission() +// guard status == .authorizedAlways || status == .authorizedWhenInUse else { return } +// +// streamTask = Task { [weak self] in +// guard let self else { return } +// +// for await location in streamUseCase.startUpdate() { +// let real = CLLocationCoordinate2D( +// latitude: location.coordinate.latitude, +// longitude: location.coordinate.longitude +// ) +// +// #if DEBUG +// // ✅ 디버그에선 mock 있으면 그걸로 덮어씀 +// if let mock = self.mockLocation { +// self.currentLocation = mock +// } else { +// self.currentLocation = real +// } +// #else +// self.currentLocation = real +// #endif +// } +// } +// } +// } func setupLocation() { requestPermissionAndStartTracking() From 9664b759d989fbb443f6004a298911bef9f9dc22 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Thu, 12 Feb 2026 15:24:08 +0900 Subject: [PATCH 12/17] =?UTF-8?q?[REFACTOR]=20=EC=83=81=EC=84=B8=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=A7=80=EB=8F=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift | 19 ++ .../DetailRouteViewController.swift | 187 +++++++++++++++--- .../DetailRoute/DetailRouteViewModel.swift | 61 ++++-- .../Location/View/TMapContainerView.swift | 6 + 4 files changed, 222 insertions(+), 51 deletions(-) diff --git a/Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift b/Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift index 02e6ba4..17021a3 100644 --- a/Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift +++ b/Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift @@ -153,6 +153,7 @@ final class TMapWrapper: NSObject, MapRendering { trafficMarkers.forEach { $0.map = nil } trafficMarkers.removeAll() } + } extension TMapWrapper: TMapViewDelegate, TmapViewLocationDelegate { @@ -180,3 +181,21 @@ extension TMapWrapper: TMapViewDelegate, TmapViewLocationDelegate { delegate?.mapView(self, didUpdateLocation: newPosition) } } + +extension TMapWrapper { + /// 현위치를 "routeInset 기준"으로 센터링하되, 화면에서 위로 50pt 올라가 보이게 + func centerUserWithRouteInset(_ location: CLLocationCoordinate2D, yOffsetUp points: CGFloat = 50) { + let dLat = 0.00015 + let dLon = 0.00015 + + let box = [ + CLLocationCoordinate2D(latitude: location.latitude - dLat, longitude: location.longitude - dLon), + CLLocationCoordinate2D(latitude: location.latitude + dLat, longitude: location.longitude + dLon) + ] + + // 기존 route inset 그대로 + bottom만 50 증가 = 화면에서 더 위로 보임 + let inset = UIEdgeInsets(top: 110, left: 30, bottom: 160 + points, right: 30) + + mapView.fitMapBoundsWithPolygons([TMapPolygon(coordinates: box)], inset: inset) + } +} diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift index a0d27a1..42dcd26 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift @@ -25,6 +25,13 @@ final class DetailRouteViewController: BaseViewController, private var registerGradient = CAGradientLayer() private let alarmRegisterButton: AtchaButton = AtchaButton(text: "막차 알람 받기", size: .h52, style: .filled(.primary), image: .bellOutlined) + private var isFollowingUser = false + private var shouldCenterToCurrentLocationOnce = false + private var lastRouteFitApplied = false + private var isAlarmFired: Bool { + UserDefaultsWrapper.shared.bool(forKey: UserDefaultsWrapper.Key.departureAlarmDidFire.rawValue) ?? false + } + override func viewDidLoad() { super.viewDidLoad() @@ -32,7 +39,9 @@ final class DetailRouteViewController: BaseViewController, setupUI() setupAutoLayout() bindView() -// + bindFollowLogic() + installMapUserGestureDetector() +// //#if DEBUG //// ✅ 용산구청(대략) - 필요하면 조금씩 조절 //viewModel.mockLocation = CLLocationCoordinate2D(latitude: 37.5326, longitude: 126.9909) @@ -42,8 +51,11 @@ final class DetailRouteViewController: BaseViewController, override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - // 레이아웃이 확정된 뒤에 호출해야 현재 프레임(절반 화면) 기준으로 정확히 맞춰짐 - mapContainerView.adjustMapToFit(coordinates: allCoordinates) + + if !isAlarmFired && !allCoordinates.isEmpty { + mapContainerView.adjustMapToFit(coordinates: allCoordinates) + } + registerGradient.frame = registerContainer.bounds } @@ -51,6 +63,11 @@ final class DetailRouteViewController: BaseViewController, AmplitudeManager.shared.trackScreen(.course_detail) } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + applyMapModeOnAppearOrAlarmChange() + } + private func setupUI(context: DetailRouteContext) { switch context { case .beforeRegister: @@ -210,35 +227,35 @@ final class DetailRouteViewController: BaseViewController, // mapContainerView.setupZoomCenter(location: offsetLocation) // } // .store(in: &cancellables) - viewModel.$currentLocation - .compactMap { $0 } - .receive(on: DispatchQueue.global(qos: .userInitiated)) - .sink { [weak self] loc in - guard let self else { return } - - let threshold: CLLocationDistance = 300.0 - - // trafficInfo와 pathInfo를 같은 leg 순서로 zip 한다는 가정 - let pairs = zip(self.viewModel.legTrafficInfo, self.viewModel.legtPathInfo) - - var near: Set = [] - - for (traffic, path) in pairs { - guard traffic.mode == path.mode else { continue } - guard let shape = path.passShape, !shape.isEmpty else { continue } - - let coords = self.convertShapeToCoords(shape) - let d = self.distanceToPolylineMeters(point: loc, polyline: coords) - if d <= threshold { - near.insert(traffic.id) - } - } - - DispatchQueue.main.async { - self.viewModel.nearLegIDs = near - } - } - .store(in: &cancellables) +// viewModel.$currentLocation +// .compactMap { $0 } +// .receive(on: DispatchQueue.global(qos: .userInitiated)) +// .sink { [weak self] loc in +// guard let self else { return } +// +// let threshold: CLLocationDistance = 300.0 +// +// // trafficInfo와 pathInfo를 같은 leg 순서로 zip 한다는 가정 +// let pairs = zip(self.viewModel.legTrafficInfo, self.viewModel.legtPathInfo) +// +// var near: Set = [] +// +// for (traffic, path) in pairs { +// guard traffic.mode == path.mode else { continue } +// guard let shape = path.passShape, !shape.isEmpty else { continue } +// +// let coords = self.convertShapeToCoords(shape) +// let d = self.distanceToPolylineMeters(point: loc, polyline: coords) +// if d <= threshold { +// near.insert(traffic.id) +// } +// } +// +// DispatchQueue.main.async { +// self.viewModel.nearLegIDs = near +// } +// } +// .store(in: &cancellables) viewModel.$nearLegIDs .receive(on: RunLoop.main) @@ -264,6 +281,45 @@ final class DetailRouteViewController: BaseViewController, .store(in: &cancellables) } + private func bindFollowLogic() { + // 위치 + viewModel.$currentLocation + .compactMap { $0 } + .receive(on: RunLoop.main) + .sink { [weak self] coord in + guard let self else { return } + + mapContainerView.updateUserMarker(location: coord) + + if isAlarmFired { + // 알람 울린 후엔 계속 따라감 + mapContainerView.setupCenter(location: coord) + return + } + + // 알람 전: 기본은 fit 고정. 단, 현위치 버튼으로 following 켰다면 이동 + if isFollowingUser { + mapContainerView.setupCenter(location: coord) + return + } + + // 알람 전 + following 꺼져있으면 center 건드리지 않음 (사용자가 보는 fit 유지) + } + .store(in: &cancellables) + + // 헤딩 + viewModel.$deviceHeading + .compactMap { $0 } + .removeDuplicates(by: { abs($0 - $1) < 2 }) + .receive(on: RunLoop.main) + .sink { [weak self] heading in + guard let self else { return } + guard isFollowingUser || isAlarmFired else { return } + mapContainerView.setHeading(heading) + } + .store(in: &cancellables) + } + private func addRouteLine(infos: [LegPathInfo]) { var shapeStrings: [String] = [] var colors: [UIColor] = [] @@ -364,6 +420,25 @@ final class DetailRouteViewController: BaseViewController, } } + private func applyMapModeOnAppearOrAlarmChange() { + if isAlarmFired { + // (2) 알람 울린 후: 무조건 따라가기 ON + isFollowingUser = true + shouldCenterToCurrentLocationOnce = true + viewModel.startHeading() + lastRouteFitApplied = false + } else { + // (1) 알람 울리기 전: 경로 전체 보이기 고정 + isFollowingUser = false + shouldCenterToCurrentLocationOnce = false + viewModel.stopHeading() + + // 화면 재진입 때마다 fit으로 "다시" 고정하려면 매번 호출 + mapContainerView.adjustMapToFit(coordinates: allCoordinates) + lastRouteFitApplied = true + } + } + deinit { activePermissionToast?.hideImmediately() mapContainerView.deinitMapView() @@ -378,11 +453,25 @@ extension DetailRouteViewController { @objc private func didTapLocationButton() { ensureLocationPermissionOrShowToast() + + isFollowingUser = true + shouldCenterToCurrentLocationOnce = true + viewModel.startHeading() + viewModel.setupLocation() } @objc private func didTapReload() { refreshButton.start() + + if !isAlarmFired { + isFollowingUser = false + shouldCenterToCurrentLocationOnce = false + viewModel.stopHeading() + + mapContainerView.adjustMapToFit(coordinates: allCoordinates) + } + viewModel.fetchInfo() AmplitudeManager.shared.track(.course_refresh_click) } @@ -503,3 +592,37 @@ extension DetailRouteViewController { return best } } + +extension DetailRouteViewController: UIGestureRecognizerDelegate { + private func installMapUserGestureDetector() { + let targetView = mapContainerView.gestureTargetView + + let pan = UIPanGestureRecognizer(target: self, action: #selector(userDidManipulateMap)) + pan.cancelsTouchesInView = false + pan.delegate = self + targetView.addGestureRecognizer(pan) + + let pinch = UIPinchGestureRecognizer(target: self, action: #selector(userDidManipulateMap)) + pinch.cancelsTouchesInView = false + pinch.delegate = self + targetView.addGestureRecognizer(pinch) + + let rotate = UIRotationGestureRecognizer(target: self, action: #selector(userDidManipulateMap)) + rotate.cancelsTouchesInView = false + rotate.delegate = self + targetView.addGestureRecognizer(rotate) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + true + } + + @objc private func userDidManipulateMap(_ g: UIGestureRecognizer) { + if g.state == .began { + isFollowingUser = false + shouldCenterToCurrentLocationOnce = false + viewModel.stopHeading() + } + } +} diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift index 1dd5937..eb30c22 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift @@ -45,16 +45,14 @@ final class DetailRouteViewModel: BaseViewModel { @Published private(set) var context: DetailRouteContext @Published var nearLegIDs: Set = [] -// + @Published var deviceHeading: CLLocationDirection? + private let headingManager = HeadingManager() + +// //#if DEBUG //@Published var mockLocation: CLLocationCoordinate2D? = nil //#endif - - deinit { - stopBusPolling() - stopSubwayPolling() - } - + init(address: String, infos: LegInfo, context: DetailRouteContext, @@ -142,21 +140,46 @@ final class DetailRouteViewModel: BaseViewModel { } func requestPermissionAndStartTracking() { - Task { - let status = await authorizationUseCase.askLocationPermission() - guard status == .authorizedAlways || status == .authorizedWhenInUse else { return } - - streamTask = Task { - for await location in streamUseCase.startUpdate() { - let currentLocation = CLLocationCoordinate2D(latitude: location.coordinate.latitude, - longitude: location.coordinate.longitude) - - self.currentLocation = currentLocation - break + Task { + let status = await authorizationUseCase.askLocationPermission() + guard status == .authorizedAlways || status == .authorizedWhenInUse else { return } + + streamTask?.cancel() + streamTask = Task { + for await location in streamUseCase.startUpdate() { + let coord = CLLocationCoordinate2D( + latitude: location.coordinate.latitude, + longitude: location.coordinate.longitude + ) + await MainActor.run { self.currentLocation = coord } + } } } } - } + + func startHeading() { + headingManager.onHeading = { [weak self] h in + DispatchQueue.main.async { self?.deviceHeading = h } + } + headingManager.start() + } + + func stopHeading() { + headingManager.stop() + } + + func stopTracking() { + streamTask?.cancel() + streamUseCase.stopUpdate() + headingManager.stop() + } + + deinit { + stopTracking() + stopBusPolling() + stopSubwayPolling() + } + // func requestPermissionAndStartTracking() { // Task { // let status = await authorizationUseCase.askLocationPermission() diff --git a/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift b/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift index e47f850..755a466 100644 --- a/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift +++ b/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift @@ -85,3 +85,9 @@ extension TMapContainerView { setupTMap() } } + +extension TMapContainerView { + func setupCenterWithRouteInsetOffset(location: CLLocationCoordinate2D, points: CGFloat = 50) { + tMapWrapper.centerUserWithRouteInset(location, yOffsetUp: points) + } +} From 4f2cb5013dae64ae7a0fe9b4deb94fba39d84a54 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Thu, 12 Feb 2026 15:41:52 +0900 Subject: [PATCH 13/17] =?UTF-8?q?[REFACTOR]=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=ED=99=95=EB=8C=80=20=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS/Presentation/Location/View/TMapContainerView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift b/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift index 755a466..e9daea4 100644 --- a/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift +++ b/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift @@ -45,7 +45,7 @@ final class TMapContainerView: UIView { func setupZoomCenter(location: CLLocationCoordinate2D) { tMapWrapper.mapView.setCenter(location) - tMapWrapper.mapView.setZoom(18) + tMapWrapper.mapView.setZoom(16) } func updateUserMarker(location: CLLocationCoordinate2D) { From e6488d802ece8ce81add34affcbf717fff72f8d1 Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Thu, 12 Feb 2026 19:53:26 +0900 Subject: [PATCH 14/17] =?UTF-8?q?[REFACTOR]=20=EC=83=81=EC=84=B8=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=A7=80=EB=8F=84=20=EC=84=BC=ED=84=B0=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift | 2 +- .../DetailRouteViewController.swift | 35 ++++++++++++++----- .../Location/View/TMapContainerView.swift | 9 ++++- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift b/Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift index 17021a3..8747e2a 100644 --- a/Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift +++ b/Atcha-iOS/Core/ViewWrapper/TMapWrapper.swift @@ -29,7 +29,7 @@ final class TMapWrapper: NSObject, MapRendering { private var trafficMarkers: [TMapMarker] = [] public init(frame: CGRect) { - self.mapView = TMapView(frame: UIScreen.main.bounds) + self.mapView = TMapView(frame: frame) super.init() configureDefaultSettings() } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift index 42dcd26..fb8e04b 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift @@ -54,8 +54,20 @@ final class DetailRouteViewController: BaseViewController, if !isAlarmFired && !allCoordinates.isEmpty { mapContainerView.adjustMapToFit(coordinates: allCoordinates) + + mapContainerView.snp.remakeConstraints { make in + make.horizontalEdges.equalToSuperview() + make.top.equalToSuperview() + make.height.equalToSuperview().multipliedBy(0.65) + } + } else if isAlarmFired { + mapContainerView.snp.remakeConstraints { make in + make.horizontalEdges.equalToSuperview() + make.top.equalToSuperview() + make.bottom.equalToSuperview().inset(200) + } } - + registerGradient.frame = registerContainer.bounds } @@ -158,11 +170,6 @@ final class DetailRouteViewController: BaseViewController, } private func setupAutoLayout() { - mapContainerView.snp.makeConstraints { make in - make.horizontalEdges.equalToSuperview() - make.top.equalToSuperview() - make.height.equalToSuperview().multipliedBy(0.65) - } backButton.snp.makeConstraints { make in make.leading.equalToSuperview().offset(16) make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(12) @@ -182,6 +189,12 @@ final class DetailRouteViewController: BaseViewController, make.trailing.equalToSuperview().inset(16) make.bottom.equalTo(view.snp.bottom).inset(40) } + + mapContainerView.snp.makeConstraints { make in + make.horizontalEdges.equalToSuperview() + make.top.equalToSuperview() + make.height.equalToSuperview().multipliedBy(0.65) + } } private func bindView() { @@ -293,13 +306,13 @@ final class DetailRouteViewController: BaseViewController, if isAlarmFired { // 알람 울린 후엔 계속 따라감 - mapContainerView.setupCenter(location: coord) + mapContainerView.setupZoomCenter(location: coord) return } // 알람 전: 기본은 fit 고정. 단, 현위치 버튼으로 following 켰다면 이동 if isFollowingUser { - mapContainerView.setupCenter(location: coord) + mapContainerView.setupZoomCenter(location: coord) return } @@ -459,6 +472,12 @@ extension DetailRouteViewController { viewModel.startHeading() viewModel.setupLocation() + + mapContainerView.snp.remakeConstraints { make in + make.horizontalEdges.equalToSuperview() + make.top.equalToSuperview() + make.bottom.equalToSuperview().inset(200) + } } @objc private func didTapReload() { diff --git a/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift b/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift index e9daea4..d0a095e 100644 --- a/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift +++ b/Atcha-iOS/Presentation/Location/View/TMapContainerView.swift @@ -30,8 +30,15 @@ final class TMapContainerView: UIView { setupTMap() } + override func layoutSubviews() { + super.layoutSubviews() + // SDK가 초기 frame 기반으로 렌더링 잡는 경우 대비 + tMapWrapper.mapView.frame = bounds + } + + private func setupTMap() { - tMapWrapper = TMapWrapper(frame: bounds) + tMapWrapper = TMapWrapper(frame: .zero) tMapWrapper.delegate = delegate addSubview(tMapWrapper.mapView) tMapWrapper.mapView.snp.makeConstraints { From 7c1c4bf81a133135838ae9ea29118b5ce177cdaa Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Thu, 12 Feb 2026 21:02:42 +0900 Subject: [PATCH 15/17] =?UTF-8?q?[REFACTOR]=20=EC=83=81=EC=84=B8=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cell/DetailRouteWalkCell.swift | 50 ++++-- .../DetailRouteInfoBottomView.swift | 47 ++++-- .../DetailRouteViewController.swift | 147 ++++++++++-------- .../DetailRoute/DetailRouteViewModel.swift | 3 +- 4 files changed, 150 insertions(+), 97 deletions(-) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteWalkCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteWalkCell.swift index 886942b..a23fd5e 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteWalkCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteWalkCell.swift @@ -18,6 +18,8 @@ final class DetailRouteWalkCell: UICollectionViewCell { private let timeLabel: UILabel = UILabel() private let distanceLabel: UILabel = UILabel() private let summaryLabel: UILabel = UILabel() + private var isArrivedEffectOn = false + var currentLegTrafficInfo: LegTrafficInfo? = nil override init(frame: CGRect) { super.init(frame: frame) @@ -32,10 +34,12 @@ final class DetailRouteWalkCell: UICollectionViewCell { override func prepareForReuse() { super.prepareForReuse() - animationView.isHidden = true - animationIconImageView.isHidden = true - animationView.stopAnimation() - backgroundColor = .clear + currentLegTrafficInfo = nil + stopArrivedEffectIfNeeded() +// animationView.isHidden = true +// animationIconImageView.isHidden = true +// animationView.stopAnimation() +// backgroundColor = .clear } private func setupUI() { @@ -74,6 +78,8 @@ final class DetailRouteWalkCell: UICollectionViewCell { } func configure(info: LegTrafficInfo?) { + currentLegTrafficInfo = info + guard let sectionTime = info?.sectionTime else { return } let timeText = AtchaFont.B6_R_14("\(sectionTime) 걷기", color: .gray200) let distanceText = AtchaFont.B6_R_14(" \(info?.distance ?? 0)m", color: .gray500) @@ -82,11 +88,34 @@ final class DetailRouteWalkCell: UICollectionViewCell { combined.append(distanceText) summaryLabel.attributedText = combined - if isCurrentTimeBetween(startTime: info?.startTime, endTime: info?.endTime) { - isNowUserLocationArrived() - } } + func isNowUserLocationArrived() { + if isArrivedEffectOn { return } + isArrivedEffectOn = true + + animationIconImageView.isHidden = false + animationView.isHidden = false + animationView.startAnimationIfNeeded(forceRestart: true) + backgroundColor = UIColor.opacity100 + } + + func stopArrivedEffectIfNeeded() { + guard isArrivedEffectOn else { + animationView.stopAnimation() + animationView.isHidden = true + animationIconImageView.isHidden = true + backgroundColor = .clear + return + } + + isArrivedEffectOn = false + animationView.stopAnimation() + animationView.isHidden = true + animationIconImageView.isHidden = true + backgroundColor = .clear + } + private func isCurrentTimeBetween(startTime: String?, endTime: String?) -> Bool { guard let startTime, let endTime else { return false } @@ -124,11 +153,4 @@ final class DetailRouteWalkCell: UICollectionViewCell { return todayNow >= todayStart && todayNow < todayEnd } } - - func isNowUserLocationArrived() { - animationIconImageView.isHidden = false - animationView.isHidden = false - animationView.startAnimationIfNeeded(forceRestart: true) - backgroundColor = UIColor.opacity100 - } } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift index 33b1ff0..29e2f69 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/DetailRouteInfoBottomView.swift @@ -43,6 +43,7 @@ final class DetailRouteInfoBottomView: UIView { private var subwayRealTimeInfo: [SubwayRealTimeInfo] = [] var onBusDetail: ((BusDetailInfo) -> Void)? var getNewBusRealTime: (() -> Void)? + private var currentNearLegIDs: Set = [] override init(frame: CGRect) { super.init(frame: frame) @@ -134,20 +135,20 @@ final class DetailRouteInfoBottomView: UIView { // v2 func setupBusTimerLabel(_ time: [[RealTimeBusArrival]]) { busRealTimeInfo = time - + for cell in collectionView.visibleCells { guard let busCell = cell as? DetailRouteBusCell else { continue } - + // 현재 셀이 어떤 route였는지 알아야 다시 매칭 가능 guard let route = busCell.currentLegTrafficInfo?.route else { continue } let cellKey = route.components(separatedBy: ":").last ?? route - + let matchedInfo = busRealTimeInfo.first(where: { list in guard let apiRaw = list.first?.routeName else { return false } let apiKey = apiRaw.components(separatedBy: ":").last ?? apiRaw return apiKey == cellKey }) ?? [] - + guard !matchedInfo.isEmpty else { continue } busCell.setupBusRealTimeInfo(info: busCell.currentLegTrafficInfo, busInfo: matchedInfo) @@ -156,11 +157,11 @@ final class DetailRouteInfoBottomView: UIView { func setupSubwayTimerLabel(_ infos: [SubwayRealTimeInfo]) { subwayRealTimeInfo = infos - + for cell in collectionView.visibleCells { guard let subwayCell = cell as? DetailRouteSubwayCell else { continue } guard let route = subwayCell.currentLegTrafficInfo?.route, !route.isEmpty else { continue } - + let matched = subwayRealTimeInfo.filter { $0.routeName == route } subwayCell.setupSubwayRealTime(routeName: route, infos: matched) } @@ -173,7 +174,11 @@ final class DetailRouteInfoBottomView: UIView { private func applySnapshot(animatingDifferences: Bool = false) { guard collectionView.dataSource != nil else { return } - dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) { [weak self] in + guard let self else { return } + self.updateProximityHighlight(nearLegIDs: self.currentNearLegIDs) + } } } @@ -336,14 +341,14 @@ extension DetailRouteInfoBottomView { cell.configure(info: item.info) if let route = item.info?.route, !route.isEmpty { - let key = route.components(separatedBy: ":").last ?? route - let matched = self.subwayRealTimeInfo.filter { info in - let apiRaw = info.routeName ?? "" - let apiKey = apiRaw.components(separatedBy: ":").last ?? apiRaw - return apiKey == key - } - cell.setupSubwayRealTime(routeName: route, infos: matched) - } + let key = route.components(separatedBy: ":").last ?? route + let matched = self.subwayRealTimeInfo.filter { info in + let apiRaw = info.routeName ?? "" + let apiKey = apiRaw.components(separatedBy: ":").last ?? apiRaw + return apiKey == key + } + cell.setupSubwayRealTime(routeName: route, infos: matched) + } return cell default: @@ -354,6 +359,7 @@ extension DetailRouteInfoBottomView { } func updateProximityHighlight(nearLegIDs: Set) { + currentNearLegIDs = nearLegIDs for cell in collectionView.visibleCells { if let busCell = cell as? DetailRouteBusCell, let leg = busCell.currentLegTrafficInfo { @@ -363,7 +369,7 @@ extension DetailRouteInfoBottomView { busCell.stopArrivedEffectIfNeeded() } } - + if let subwayCell = cell as? DetailRouteSubwayCell, let leg = subwayCell.currentLegTrafficInfo { if nearLegIDs.contains(leg.id) { @@ -372,6 +378,15 @@ extension DetailRouteInfoBottomView { subwayCell.stopArrivedEffectIfNeeded() } } + + if let walkCell = cell as? DetailRouteWalkCell, + let leg = walkCell.currentLegTrafficInfo { + if nearLegIDs.contains(leg.id) { + walkCell.isNowUserLocationArrived() + } else { + walkCell.stopArrivedEffectIfNeeded() + } + } } } } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift index fb8e04b..2cede17 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewController.swift @@ -10,6 +10,7 @@ import CoreLocation import TMapSDK import VSMSDK import MapKit +import Combine final class DetailRouteViewController: BaseViewController, TMapWrapperDelegate { @@ -31,6 +32,7 @@ final class DetailRouteViewController: BaseViewController, private var isAlarmFired: Bool { UserDefaultsWrapper.shared.bool(forKey: UserDefaultsWrapper.Key.departureAlarmDidFire.rawValue) ?? false } + private var legPolylineById: [UUID: [CLLocationCoordinate2D]] = [:] override func viewDidLoad() { super.viewDidLoad() @@ -41,12 +43,12 @@ final class DetailRouteViewController: BaseViewController, bindView() bindFollowLogic() installMapUserGestureDetector() -// //#if DEBUG -//// ✅ 용산구청(대략) - 필요하면 조금씩 조절 -//viewModel.mockLocation = CLLocationCoordinate2D(latitude: 37.5326, longitude: 126.9909) +//// ✅ 서울아산병원(대략) +//viewModel.mockLocation = CLLocationCoordinate2D(latitude:37.566956, longitude: 126.979406) //viewModel.currentLocation = viewModel.mockLocation //#endif + } override func viewDidLayoutSubviews() { @@ -146,13 +148,6 @@ final class DetailRouteViewController: BaseViewController, backButton.setCornerRadius(18) backButton.addTarget(self, action: #selector(didTapClose), for: .touchUpInside) - // relaodButton.setImage(UIImage.refreshOutlined, for: .normal) - // relaodButton.tintColor = .white - // relaodButton.backgroundColor = .gray600 - // relaodButton.clipsToBounds = true - // relaodButton.setCornerRadius(24) - // relaodButton.addTarget(self, action: #selector(didTapReload), for: .touchUpInside) - refreshButton.isUserInteractionEnabled = true let tap = UITapGestureRecognizer(target: self, action: #selector(didTapReload)) refreshButton.addGestureRecognizer(tap) @@ -201,7 +196,11 @@ final class DetailRouteViewController: BaseViewController, viewModel.$legTrafficInfo .receive(on: RunLoop.main) .compactMap { $0 } - .sink { [weak self] infos in self?.bottomSheet.setupRouteInfo(infos) } + .sink { [weak self] infos in + guard let self else { return } + self.bottomSheet.setupRouteInfo(infos) + self.bottomSheet.updateProximityHighlight(nearLegIDs: self.viewModel.nearLegIDs) + } .store(in: &cancellables) viewModel.$busRealTimeInfos @@ -225,51 +224,6 @@ final class DetailRouteViewController: BaseViewController, .sink { [weak self] address in self?.bottomSheet.setupStartAddress(address) } .store(in: &cancellables) -// viewModel.$currentLocation -// .compactMap { $0 } -// .receive(on: DispatchQueue.main) -// .sink { [weak self] location in -// guard let self else { return } -// mapContainerView.updateUserMarker(location: location) -// let offsetLatitude = location.latitude - 0.0003 -// let offsetLocation = CLLocationCoordinate2D( -// latitude: offsetLatitude, -// longitude: location.longitude -// ) -// -// mapContainerView.setupZoomCenter(location: offsetLocation) -// } -// .store(in: &cancellables) -// viewModel.$currentLocation -// .compactMap { $0 } -// .receive(on: DispatchQueue.global(qos: .userInitiated)) -// .sink { [weak self] loc in -// guard let self else { return } -// -// let threshold: CLLocationDistance = 300.0 -// -// // trafficInfo와 pathInfo를 같은 leg 순서로 zip 한다는 가정 -// let pairs = zip(self.viewModel.legTrafficInfo, self.viewModel.legtPathInfo) -// -// var near: Set = [] -// -// for (traffic, path) in pairs { -// guard traffic.mode == path.mode else { continue } -// guard let shape = path.passShape, !shape.isEmpty else { continue } -// -// let coords = self.convertShapeToCoords(shape) -// let d = self.distanceToPolylineMeters(point: loc, polyline: coords) -// if d <= threshold { -// near.insert(traffic.id) -// } -// } -// -// DispatchQueue.main.async { -// self.viewModel.nearLegIDs = near -// } -// } -// .store(in: &cancellables) - viewModel.$nearLegIDs .receive(on: RunLoop.main) .sink { [weak self] near in @@ -292,6 +246,40 @@ final class DetailRouteViewController: BaseViewController, self?.setupUI(context: ctx) } .store(in: &cancellables) + + Publishers.CombineLatest(viewModel.$legTrafficInfo, viewModel.$legtPathInfo) + .receive(on: DispatchQueue.global(qos: .userInitiated)) + .sink { [weak self] trafficInfos, pathInfos in + guard let self else { return } + guard !trafficInfos.isEmpty, !pathInfos.isEmpty else { return } + + var dict: [UUID: [CLLocationCoordinate2D]] = [:] + + for (traffic, path) in zip(trafficInfos, pathInfos) { + guard traffic.mode == path.mode else { continue } + + if let shape = path.passShape, !shape.isEmpty { + dict[traffic.id] = self.convertShapeToCoords(shape) + continue + } + + if let steps = path.step, !steps.isEmpty { + let merged = steps + .compactMap { $0.linestring } + .filter { !$0.isEmpty } + .joined(separator: " ") + + if !merged.isEmpty { + dict[traffic.id] = self.convertShapeToCoords(merged) + } + } + } + + DispatchQueue.main.async { [weak self] in + self?.legPolylineById = dict + } + } + .store(in: &cancellables) } private func bindFollowLogic() { @@ -302,21 +290,48 @@ final class DetailRouteViewController: BaseViewController, .sink { [weak self] coord in guard let self else { return } - mapContainerView.updateUserMarker(location: coord) + // 1) 유저 마커 업데이트 (메인) + self.mapContainerView.updateUserMarker(location: coord) - if isAlarmFired { + // 2) 지도 follow 로직 (메인) + if self.isAlarmFired { // 알람 울린 후엔 계속 따라감 - mapContainerView.setupZoomCenter(location: coord) - return + self.mapContainerView.setupZoomCenter(location: coord) + } else if self.isFollowingUser { + // 알람 전: following 켰을 때만 따라감 + self.mapContainerView.setupZoomCenter(location: coord) } + // else: fit 유지 (건드리지 않음) - // 알람 전: 기본은 fit 고정. 단, 현위치 버튼으로 following 켰다면 이동 - if isFollowingUser { - mapContainerView.setupZoomCenter(location: coord) - return - } + // 3) 근처(150m) 지나가면 반짝임 계산 (백그라운드) + let threshold: CLLocationDistance = 150 + + let polylines = self.legPolylineById + let orderedLegs = self.viewModel.legTrafficInfo // ✅ 화면 표시 순서(위→아래) - // 알람 전 + following 꺼져있으면 center 건드리지 않음 (사용자가 보는 fit 유지) + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self else { return } + + // 1) near 후보들을 Set으로 수집 + var nearCandidates = Set() + + for (id, polyline) in polylines { + let d = self.distanceToPolylineMeters(point: coord, polyline: polyline) + if d <= threshold { + nearCandidates.insert(id) + } + } + + // 2) ✅ "위에 있는 셀 우선" = orderedLegs 순서로 첫 매칭 1개만 남김 + var picked: Set = [] + if let first = orderedLegs.first(where: { nearCandidates.contains($0.id) })?.id { + picked = [first] + } + + DispatchQueue.main.async { [weak self] in + self?.viewModel.nearLegIDs = picked + } + } } .store(in: &cancellables) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift index eb30c22..d043f99 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteViewModel.swift @@ -48,7 +48,7 @@ final class DetailRouteViewModel: BaseViewModel { @Published var deviceHeading: CLLocationDirection? private let headingManager = HeadingManager() -// + //#if DEBUG //@Published var mockLocation: CLLocationCoordinate2D? = nil //#endif @@ -78,6 +78,7 @@ final class DetailRouteViewModel: BaseViewModel { func fetchInfo() { legtPathInfo = infos.pathInfo legTrafficInfo = infos.trafficInfo + print("위치: \(legtPathInfo)") guard context == .afterReigster else { return } // 버스 From b96367f28727482877b8861472cee5ea7e9c860a Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Thu, 12 Feb 2026 21:06:03 +0900 Subject: [PATCH 16/17] =?UTF-8?q?[REFACTOR]=20=EC=8B=A4=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=20=EC=8B=9C=EA=B0=84=EB=B6=84=EC=B4=88?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Cell/DetailRouteBusCell.swift | 31 +++++++++++++++---- .../Cell/DetailRouteSubwayCell.swift | 28 ++++++++++++++--- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift index 7d552a7..d3784af 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift @@ -508,7 +508,7 @@ extension DetailRouteBusCell { if remaining <= 120 { return AtchaFont.B6_R_14("곧 도착", color: .widearea) } else { - return AtchaFont.B6_R_14(formatSecondsToMinutesAndSeconds(remaining), color: .widearea) + return AtchaFont.B6_R_14(formatSecondsToHMS(remaining), color: .widearea) } } @@ -526,10 +526,29 @@ extension DetailRouteBusCell { } } - private func formatSecondsToMinutesAndSeconds(_ seconds: Int?) -> String { - guard let seconds = seconds, seconds >= 0 else { return "시간 없음" } - let minutes = seconds / 60 - let remainingSeconds = seconds % 60 - return "\(minutes)분 \(remainingSeconds)초" + private func formatSecondsToHMS(_ seconds: Int?) -> String { + guard let seconds, seconds >= 0 else { return "시간 없음" } + + let h = seconds / 3600 + let m = (seconds % 3600) / 60 + let s = seconds % 60 + + if h > 0 { + // 시가 있으면 시/분/초 + // (원하면 "1시간 0분 5초"처럼 0분도 보여줄지 결정 가능) + if m > 0 { + return "\(h)시간 \(m)분 \(s)초" + } else { + return "\(h)시간 \(s)초" + } + } + + if m > 0 { + // 시가 없으면 분/초 + return "\(m)분 \(s)초" + } + + // 분도 없으면 초만 + return "\(s)초" } } diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift index 555450c..67c93d0 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift @@ -498,12 +498,32 @@ extension DetailRouteSubwayCell { return } - subwayTimerLabel.attributedText = AtchaFont.B6_R_14(formatMinSecKorean(sec), color: .widearea) + subwayTimerLabel.attributedText = AtchaFont.B6_R_14(formatSecondsToHMS(sec), color: .widearea) } - private func formatMinSecKorean(_ seconds: Int) -> String { - let m = seconds / 60 + private func formatSecondsToHMS(_ seconds: Int?) -> String { + guard let seconds, seconds >= 0 else { return "시간 없음" } + + let h = seconds / 3600 + let m = (seconds % 3600) / 60 let s = seconds % 60 - return "\(m)분 \(s)초" + + if h > 0 { + // 시가 있으면 시/분/초 + // (원하면 "1시간 0분 5초"처럼 0분도 보여줄지 결정 가능) + if m > 0 { + return "\(h)시간 \(m)분 \(s)초" + } else { + return "\(h)시간 \(s)초" + } + } + + if m > 0 { + // 시가 없으면 분/초 + return "\(m)분 \(s)초" + } + + // 분도 없으면 초만 + return "\(s)초" } } From 439eb03e5bcd6c08108a0c6efdececdf90baa26b Mon Sep 17 00:00:00 2001 From: JaeWoong Eum Date: Fri, 13 Feb 2026 10:22:09 +0900 Subject: [PATCH 17/17] =?UTF-8?q?[REFACTOR]=20=EB=B2=84=EC=8A=A4,=20?= =?UTF-8?q?=EC=A7=80=ED=95=98=EC=B2=A0=20=EC=A0=95=EB=B3=B4=EC=97=86?= =?UTF-8?q?=EC=9D=8C=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Atcha-iOS.xcodeproj/project.pbxproj | 4 ++-- .../Cell/DetailRouteBusCell.swift | 22 +++++++++---------- .../Cell/DetailRouteSubwayCell.swift | 19 +++++++++------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Atcha-iOS.xcodeproj/project.pbxproj b/Atcha-iOS.xcodeproj/project.pbxproj index c3510b1..6baace4 100644 --- a/Atcha-iOS.xcodeproj/project.pbxproj +++ b/Atcha-iOS.xcodeproj/project.pbxproj @@ -2349,7 +2349,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.5; + MARKETING_VERSION = 1.7; PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2397,7 +2397,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.5; + MARKETING_VERSION = 1.7; PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift index d3784af..f6c48a1 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteBusCell.swift @@ -49,9 +49,9 @@ final class DetailRouteBusCell: UICollectionViewCell { private let stationListStackView = UIStackView() private let busTimerFirstLabel: UILabel = UILabel() - private let busTimerSecondLabel: UILabel = UILabel() +// private let busTimerSecondLabel: UILabel = UILabel() private lazy var busTimerStackView: UIStackView = { - let stack = UIStackView(arrangedSubviews: [busTimerFirstLabel, busTimerSecondLabel]) + let stack = UIStackView(arrangedSubviews: [busTimerFirstLabel]) stack.axis = .vertical stack.alignment = .leading stack.spacing = 4 @@ -435,15 +435,13 @@ extension DetailRouteBusCell { guard !busInfo.isEmpty else { stopCountdownTimer() - busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("정보 없음", color: .gray300) - busTimerSecondLabel.text = "" + busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("", color: .gray300) return } guard !currentBusInfo.isEmpty else { stopCountdownTimer() - busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("도착 또는 출발", color: .widearea) - busTimerSecondLabel.text = "" + busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("", color: .widearea) return } @@ -487,7 +485,7 @@ extension DetailRouteBusCell { if currentBusInfo.isEmpty { stopCountdownTimer() busTimerFirstLabel.attributedText = AtchaFont.B6_R_14("도착 또는 출발", color: .widearea) - busTimerSecondLabel.text = "" +// busTimerSecondLabel.text = "" return } @@ -502,7 +500,7 @@ extension DetailRouteBusCell { } guard let remaining = info.remainingTime else { - return AtchaFont.B6_R_14("정보 없음", color: .gray300) + return AtchaFont.B6_R_14("", color: .gray300) } if remaining <= 120 { @@ -515,19 +513,19 @@ extension DetailRouteBusCell { switch currentBusInfo.count { case 2: busTimerFirstLabel.attributedText = labelText(for: currentBusInfo[0]) - busTimerSecondLabel.attributedText = labelText(for: currentBusInfo[1]) +// busTimerSecondLabel.attributedText = labelText(for: currentBusInfo[1]) case 1: busTimerFirstLabel.attributedText = labelText(for: currentBusInfo[0]) - busTimerSecondLabel.text = "" +// busTimerSecondLabel.text = "" default: // 3개 이상이면 우선 2개만 보여주거나, 숨기지 말고 2개만 보여주자 busTimerFirstLabel.attributedText = labelText(for: currentBusInfo[0]) - busTimerSecondLabel.attributedText = labelText(for: currentBusInfo[1]) +// busTimerSecondLabel.attributedText = labelText(for: currentBusInfo[1]) } } private func formatSecondsToHMS(_ seconds: Int?) -> String { - guard let seconds, seconds >= 0 else { return "시간 없음" } + guard let seconds, seconds >= 0 else { return "" } let h = seconds / 3600 let m = (seconds % 3600) / 60 diff --git a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift index 67c93d0..d6a438b 100644 --- a/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift +++ b/Atcha-iOS/Presentation/Location/DetailRoute/DetailRouteInfo/Cell/DetailRouteSubwayCell.swift @@ -415,7 +415,7 @@ extension DetailRouteSubwayCell { subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("", color: .white) guard let routeName, !routeName.isEmpty else { - subwayTimerLabel.attributedText = AtchaFont.B6_R_14("정보 없음", color: .gray300) + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("", color: .gray300) return } @@ -428,16 +428,19 @@ extension DetailRouteSubwayCell { } guard let matched else { - subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("정보 없음", color: .gray300) - subwayTimerLabel.attributedText = AtchaFont.B6_R_14("정보 없음", color: .gray300) + subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("", color: .gray300) + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("", color: .gray300) return } - let destination = matched.destination ?? "" - subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("\(destination)행", color: .white) + if let destination = matched.destination { + subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("\(destination)행", color: .white) + } else { + subwayDirectionLabel.attributedText = AtchaFont.B6_R_14("", color: .white) + } guard let sec = matched.remainingTime, sec >= 0 else { - subwayTimerLabel.attributedText = AtchaFont.B6_R_14("불러오는 중", color: .widearea) + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("", color: .widearea) return } @@ -484,7 +487,7 @@ extension DetailRouteSubwayCell { private func updateSubwayTimerLabel() { guard let sec = currentRemainingSec else { - subwayTimerLabel.attributedText = AtchaFont.B6_R_14("불러오는 중", color: .widearea) + subwayTimerLabel.attributedText = AtchaFont.B6_R_14("", color: .widearea) return } @@ -502,7 +505,7 @@ extension DetailRouteSubwayCell { } private func formatSecondsToHMS(_ seconds: Int?) -> String { - guard let seconds, seconds >= 0 else { return "시간 없음" } + guard let seconds, seconds >= 0 else { return "" } let h = seconds / 3600 let m = (seconds % 3600) / 60