From 772db8e06f88d7d5372f8c97b3644cd3cc3268da Mon Sep 17 00:00:00 2001 From: minneee Date: Mon, 15 Dec 2025 18:02:06 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[feat]=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=20=EB=B7=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/Travel/TravelListSkeletonView.swift | 85 +++++++++++++++++++ .../SkeletonShimmerModifier.swift | 4 +- 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 DesignSystem/Sources/View/Travel/TravelListSkeletonView.swift diff --git a/DesignSystem/Sources/View/Travel/TravelListSkeletonView.swift b/DesignSystem/Sources/View/Travel/TravelListSkeletonView.swift new file mode 100644 index 00000000..c290d13f --- /dev/null +++ b/DesignSystem/Sources/View/Travel/TravelListSkeletonView.swift @@ -0,0 +1,85 @@ +public struct TravelListSkeletonView: View { + @State private var shimmerPhase: CGFloat = -1.0 + + var body: some View { + ScrollView(showsIndicators: false) { + LazyVStack(spacing: 18) { + ForEach(0..<5, id: \.self) { _ in + cardSkeleton() + } + } + .padding(16) + } + .onAppear { + withAnimation( + .linear(duration: 1.2) + .repeatForever(autoreverses: false) + ) { + shimmerPhase = 1.2 + } + } + } + + private func cardSkeleton() -> some View { + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 16) + .fill(Color.primary100) + + RoundedRectangle(cornerRadius: 16) + .fill(Color.appWhite) + .offset(x: 5) + + VStack(alignment: .leading, spacing: 12) { + capsulePlaceholder(width: 56, height: 18) + + fullBar(height: 20) + + HStack(spacing: 12) { + iconPlaceholder() + smallBar(width: 120) + Divider() + .frame(width: 1, height: 16) + .overlay(Color.gray2) + iconPlaceholder() + smallBar(width: 60) + } + + HStack(spacing: 12) { + smallBar(width: 80) + smallBar(width: 50) + Spacer() + } + } + .padding(20) + } + .fixedSize(horizontal: false, vertical: true) + } + + private func capsulePlaceholder(width: CGFloat, height: CGFloat) -> some View { + Capsule() + .fill(Color.gray2) + .frame(width: width, height: height) + .skeletonShimmer(phase: shimmerPhase) + } + + private func smallBar(width: CGFloat, height: CGFloat = 12) -> some View { + RoundedRectangle(cornerRadius: 6) + .fill(Color.gray2) + .frame(width: width, height: height) + .skeletonShimmer(phase: shimmerPhase) + } + + private func fullBar(height: CGFloat) -> some View { + RoundedRectangle(cornerRadius: 6) + .fill(Color.gray2) + .frame(maxWidth: .infinity, minHeight: height, maxHeight: height) + .skeletonShimmer(phase: shimmerPhase) + } + + private func iconPlaceholder(size: CGFloat = 18) -> some View { + RoundedRectangle(cornerRadius: 4) + .fill(Color.gray2) + .frame(width: size, height: size) + .skeletonShimmer(phase: shimmerPhase) + } +} diff --git a/DesignSystem/Sources/ViewModifier/SkeletonShimmerModifier.swift b/DesignSystem/Sources/ViewModifier/SkeletonShimmerModifier.swift index 18f2396c..5a003313 100644 --- a/DesignSystem/Sources/ViewModifier/SkeletonShimmerModifier.swift +++ b/DesignSystem/Sources/ViewModifier/SkeletonShimmerModifier.swift @@ -15,13 +15,13 @@ public extension View { LinearGradient( colors: [ .gray2.opacity(0.2), - .white.opacity(0.5), + .gray2.opacity(0.4), .gray2.opacity(0.2) ], startPoint: .leading, endPoint: .trailing ) - .frame(width: proxy.size.width * 1.2) + .frame(width: proxy.size.width * 0.8) .offset(x: proxy.size.width * phase) } .clipped() From 07de34f563c11de05dae44870a8d660e61ad2467 Mon Sep 17 00:00:00 2001 From: minneee Date: Mon, 15 Dec 2025 18:04:58 +0900 Subject: [PATCH 02/13] =?UTF-8?q?[feat]=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=20=EB=B7=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/Travel/TravelListSkeletonView.swift | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/DesignSystem/Sources/View/Travel/TravelListSkeletonView.swift b/DesignSystem/Sources/View/Travel/TravelListSkeletonView.swift index c290d13f..12ce5f2d 100644 --- a/DesignSystem/Sources/View/Travel/TravelListSkeletonView.swift +++ b/DesignSystem/Sources/View/Travel/TravelListSkeletonView.swift @@ -1,7 +1,19 @@ +// +// TravelListSkeletonView.swift +// DesignSystem +// +// Created by 김민희 on 12/15/25. +// + +import SwiftUI + +/// 여행 카드 리스트가 로딩될 때 TravelCardView의 형태를 흉내 내는 스켈레톤 뷰. public struct TravelListSkeletonView: View { @State private var shimmerPhase: CGFloat = -1.0 - var body: some View { + public init() {} + + public var body: some View { ScrollView(showsIndicators: false) { LazyVStack(spacing: 18) { ForEach(0..<5, id: \.self) { _ in @@ -12,7 +24,7 @@ public struct TravelListSkeletonView: View { } .onAppear { withAnimation( - .linear(duration: 1.2) + .linear(duration: 1.8) .repeatForever(autoreverses: false) ) { shimmerPhase = 1.2 @@ -39,16 +51,10 @@ public struct TravelListSkeletonView: View { smallBar(width: 120) Divider() .frame(width: 1, height: 16) - .overlay(Color.gray2) + .overlay(Color.gray1) iconPlaceholder() smallBar(width: 60) } - - HStack(spacing: 12) { - smallBar(width: 80) - smallBar(width: 50) - Spacer() - } } .padding(20) } @@ -57,29 +63,33 @@ public struct TravelListSkeletonView: View { private func capsulePlaceholder(width: CGFloat, height: CGFloat) -> some View { Capsule() - .fill(Color.gray2) + .fill(Color.gray1) .frame(width: width, height: height) .skeletonShimmer(phase: shimmerPhase) } private func smallBar(width: CGFloat, height: CGFloat = 12) -> some View { RoundedRectangle(cornerRadius: 6) - .fill(Color.gray2) + .fill(Color.gray1) .frame(width: width, height: height) .skeletonShimmer(phase: shimmerPhase) } private func fullBar(height: CGFloat) -> some View { RoundedRectangle(cornerRadius: 6) - .fill(Color.gray2) + .fill(Color.gray1) .frame(maxWidth: .infinity, minHeight: height, maxHeight: height) .skeletonShimmer(phase: shimmerPhase) } private func iconPlaceholder(size: CGFloat = 18) -> some View { RoundedRectangle(cornerRadius: 4) - .fill(Color.gray2) + .fill(Color.gray1) .frame(width: size, height: size) .skeletonShimmer(phase: shimmerPhase) } } + +#Preview { + TravelListSkeletonView() +} From bd65abe78eda163e9e41424623b2ed2f58aee421 Mon Sep 17 00:00:00 2001 From: minneee Date: Mon, 15 Dec 2025 19:44:12 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[feat]=20fetchTravel=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=20DTO,=20DataSource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Data/Sources/DTO/Travel/TravelCacheDTO.swift | 132 ++++++++++++++++ .../Local/TravelLocalDataSource.swift | 143 ++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 Data/Sources/DTO/Travel/TravelCacheDTO.swift create mode 100644 Data/Sources/DataSource/Local/TravelLocalDataSource.swift diff --git a/Data/Sources/DTO/Travel/TravelCacheDTO.swift b/Data/Sources/DTO/Travel/TravelCacheDTO.swift new file mode 100644 index 00000000..d9308988 --- /dev/null +++ b/Data/Sources/DTO/Travel/TravelCacheDTO.swift @@ -0,0 +1,132 @@ +// +// TravelCacheDTO.swift +// Data +// +// Created by 김민희 on 12/15/25. +// + +import Foundation +import Domain + +public struct TravelCacheDTO: Codable { + let statusRawValue: String + let cachedAt: Date + let travels: [TravelCacheItemDTO] + + var status: TravelStatus { + TravelStatus(rawValue: statusRawValue) ?? .unknown + } + + var isExpired: Bool { + Date().timeIntervalSince(cachedAt) > TravelCacheConstants.expiration + } +} + +public struct TravelCacheItemDTO: Codable { + let id: String + let title: String + let startDate: Date + let endDate: Date + let countryCode: String + let koreanCountryName: String + let baseCurrency: String + let baseExchangeRate: Double + let destinationCurrency: String + let inviteCode: String? + let deepLink: String? + let statusRawValue: String + let role: String? + let createdAt: Date + let ownerName: String + let members: [TravelCacheMemberDTO] + let currencies: [String]? + + var status: TravelStatus { + TravelStatus(rawValue: statusRawValue) ?? .unknown + } +} + +struct TravelCacheMemberDTO: Codable { + let id: String + let name: String + let role: String + let email: String? + let avatarUrl: String? +} + +enum TravelCacheConstants { + static let directoryName = "travels" + static let expiration: TimeInterval = 60 * 60 * 6 // 6 hours +} + +extension TravelCacheItemDTO { + func toDomain() -> Travel { + Travel( + id: id, + title: title, + startDate: startDate, + endDate: endDate, + countryCode: countryCode, + koreanCountryName: koreanCountryName, + baseCurrency: baseCurrency, + baseExchangeRate: baseExchangeRate, + destinationCurrency: destinationCurrency, + inviteCode: inviteCode, + deepLink: deepLink, + status: status, + role: role, + createdAt: createdAt, + ownerName: ownerName, + members: members.map { $0.toDomain() }, + currencies: currencies + ) + } +} + +extension TravelCacheMemberDTO { + func toDomain() -> TravelMember { + TravelMember( + id: id, + name: name, + role: MemberRole(value: role), + email: email, + avatarUrl: avatarUrl + ) + } +} + +extension Travel { + func toCacheItem() -> TravelCacheItemDTO { + TravelCacheItemDTO( + id: id, + title: title, + startDate: startDate, + endDate: endDate, + countryCode: countryCode, + koreanCountryName: koreanCountryName, + baseCurrency: baseCurrency, + baseExchangeRate: baseExchangeRate, + destinationCurrency: destinationCurrency, + inviteCode: inviteCode, + deepLink: deepLink, + statusRawValue: status.rawValue, + role: role, + createdAt: createdAt, + ownerName: ownerName, + members: members.map { $0.toCacheItem() }, + currencies: currencies + ) + } +} + +extension TravelMember { + func toCacheItem() -> TravelCacheMemberDTO { + TravelCacheMemberDTO( + id: id, + name: name, + role: role.rawValue, + email: email, + avatarUrl: avatarUrl + ) + } +} diff --git a/Data/Sources/DataSource/Local/TravelLocalDataSource.swift b/Data/Sources/DataSource/Local/TravelLocalDataSource.swift new file mode 100644 index 00000000..d5478d97 --- /dev/null +++ b/Data/Sources/DataSource/Local/TravelLocalDataSource.swift @@ -0,0 +1,143 @@ +// +// TravelLocalDataSource.swift +// Data +// +// Created by 김민희 on 12/15/25. +// + +import Foundation +import Domain + +public enum TravelCacheError: Error { + case cacheDirectoryUnavailable + case fileNotFound + case dataCorrupted +} + +public protocol TravelLocalDataSourceProtocol: Actor { + func observe(status: TravelStatus) -> AsyncStream<[TravelCacheItemDTO]> + func load(status: TravelStatus) async throws -> TravelCacheDTO? + func save(_ cache: TravelCacheDTO) async throws + func clear(status: TravelStatus) +} + +public actor TravelLocalDataSource: TravelLocalDataSourceProtocol { + private let fileManager = FileManager.default + private lazy var cacheDirectory: URL? = { + guard var cachesURL = fileManager.urls( + for: .cachesDirectory, + in: .userDomainMask + ).first else { + return nil + } + cachesURL.append( + path: TravelCacheConstants.directoryName, + directoryHint: .isDirectory + ) + try? fileManager.createDirectory( + at: cachesURL, + withIntermediateDirectories: true + ) + return cachesURL + }() + + private let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + return encoder + }() + + private let decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + }() + + // 여행 상태별로 AsyncStream 보관 + private var observers: [TravelStatus: [UUID: AsyncStream<[TravelCacheItemDTO]>.Continuation]] = [:] + + public init() {} + + public func observe(status: TravelStatus) -> AsyncStream<[TravelCacheItemDTO]> { + // 캐시 파일이 갱신될 때마다 새로운 배열을 내보내며, 가지고 있는 캐시가 있다면 즉시 전달한다. + AsyncStream { continuation in + Task { [weak self] in + guard let self else { return } + let id = UUID() + continuation.onTermination = { [weak self] _ in + Task { await self?.removeObserver(status: status, id: id) } + } + await self.storeObserver(status: status, id: id, continuation: continuation) + if let cache = try? await self.load(status: status) { + continuation.yield(cache.travels) + } + } + } + } + + public func load(status: TravelStatus) async throws -> TravelCacheDTO? { + let url = try cacheURL(for: status) + guard fileManager.fileExists(atPath: url.path()) else { + return nil + } + + do { + let data = try Data(contentsOf: url) + let cache = try decoder.decode(TravelCacheDTO.self, from: data) + if cache.isExpired { + try? fileManager.removeItem(at: url) + return nil + } + return cache + } catch { + try? fileManager.removeItem(at: url) + throw TravelCacheError.dataCorrupted + } + } + + public func save(_ cache: TravelCacheDTO) async throws { + let url = try cacheURL(for: cache.status) + let data = try encoder.encode(cache) + try data.write(to: url, options: [.atomic]) + notifyObservers(status: cache.status, travels: cache.travels) + } + + public func clear(status: TravelStatus) { + guard let url = try? cacheURL(for: status) else { return } + try? fileManager.removeItem(at: url) + } +} + +private extension TravelLocalDataSource { + func cacheURL(for status: TravelStatus) throws -> URL { + guard var directory = cacheDirectory else { + throw TravelCacheError.cacheDirectoryUnavailable + } + directory.append( + path: "travel_\(status.rawValue).json", + directoryHint: .notDirectory + ) + return directory + } + + func storeObserver( + status: TravelStatus, + id: UUID, + continuation: AsyncStream<[TravelCacheItemDTO]>.Continuation + ) { + var continuations = observers[status] ?? [:] + continuations[id] = continuation + observers[status] = continuations + } + + func removeObserver(status: TravelStatus, id: UUID) { + var continuations = observers[status] ?? [:] + continuations[id] = nil + observers[status] = continuations.isEmpty ? nil : continuations + } + + func notifyObservers(status: TravelStatus, travels: [TravelCacheItemDTO]) { + guard let continuations = observers[status] else { return } + continuations.values.forEach { $0.yield(travels) } + } +} From d22e4e2f5fd7cb02e42746de08690edf772c72d9 Mon Sep 17 00:00:00 2001 From: minneee Date: Mon, 15 Dec 2025 19:44:37 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[feat]=20fetchTravel=EC=BA=90=EC=8B=9C=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/Travel/TravelRepository.swift | 70 +++++++++- .../Travel/MockTravelRepository.swift | 8 ++ .../Travel/TravelRepositoryProtocol.swift | 1 + .../Travel/ObserveTravelCacheUseCase.swift | 46 +++++++ .../Travel/Demo/Sources/TravelDemoApp.swift | 6 +- .../Sources/Reducer/TravelListFeature.swift | 122 ++++++++++++++---- .../Sources/View/Travels/TravelView.swift | 39 +++--- .../Application/LiveDependencies.swift | 6 +- 8 files changed, 248 insertions(+), 50 deletions(-) create mode 100644 Domain/Sources/UseCase/Travel/ObserveTravelCacheUseCase.swift diff --git a/Data/Sources/Repository/Travel/TravelRepository.swift b/Data/Sources/Repository/Travel/TravelRepository.swift index 33614c36..73151011 100644 --- a/Data/Sources/Repository/Travel/TravelRepository.swift +++ b/Data/Sources/Repository/Travel/TravelRepository.swift @@ -11,9 +11,14 @@ import Domain public final class TravelRepository: TravelRepositoryProtocol { private let remote: TravelRemoteDataSourceProtocol + private let local: TravelLocalDataSourceProtocol - public init(remote: TravelRemoteDataSourceProtocol) { + public init( + remote: TravelRemoteDataSourceProtocol, + local: TravelLocalDataSourceProtocol = TravelLocalDataSource() + ) { self.remote = remote + self.local = local } public func fetchTravels( @@ -21,7 +26,16 @@ public final class TravelRepository: TravelRepositoryProtocol { ) async throws -> [Travel] { let requestDTO = input.toDTO() let dtoList = try await remote.fetchTravels(body: requestDTO).items - return dtoList.map { $0.toDomain() } + let travels = dtoList.map { $0.toDomain() } + if let status = input.status { + let appendExisting = input.page > 1 + try await persistCache( + travels: travels, + status: status, + appendExisting: appendExisting + ) + } + return travels } public func createTravel( @@ -53,4 +67,56 @@ public final class TravelRepository: TravelRepositoryProtocol { let responseDTO = try await remote.fetchTravelDetail(id: id) return responseDTO.toDomain() } + + // 로컬 캐시 파일이 갱신될 때마다 스트림을 통해 최신 Travel 배열을 방출 + public func observeCachedTravels(status: TravelStatus) -> AsyncStream<[Travel]> { + AsyncStream { continuation in + let task = Task { + let baseStream = await local.observe(status: status) + for await cacheItems in baseStream { + guard !Task.isCancelled else { break } + let travels = cacheItems.map { $0.toDomain() } + continuation.yield(travels) + } + continuation.finish() + } + + continuation.onTermination = { _ in + task.cancel() + } + } + } +} + +private extension TravelRepository { + func persistCache( + travels: [Travel], + status: TravelStatus, + appendExisting: Bool + ) async throws { + var cacheItems = travels.map { $0.toCacheItem() } + + if appendExisting, + let existing = try? await local.load(status: status)?.travels { + cacheItems = existing + cacheItems + } + + let deduped = deduplicate(items: cacheItems) + let cache = TravelCacheDTO( + statusRawValue: status.rawValue, + cachedAt: Date(), + travels: deduped + ) + try await local.save(cache) + } + + // 한 번 캐시된 여행은 다시 저장하지 않도록 ID 기반으로 중복을 제거한다. + func deduplicate(items: [TravelCacheItemDTO]) -> [TravelCacheItemDTO] { + var seen = Set() + return items.filter { item in + guard !seen.contains(item.id) else { return false } + seen.insert(item.id) + return true + } + } } diff --git a/Domain/Sources/Repository/Travel/MockTravelRepository.swift b/Domain/Sources/Repository/Travel/MockTravelRepository.swift index 27575ea7..07c4b91d 100644 --- a/Domain/Sources/Repository/Travel/MockTravelRepository.swift +++ b/Domain/Sources/Repository/Travel/MockTravelRepository.swift @@ -96,6 +96,14 @@ public final class MockTravelRepository: TravelRepositoryProtocol { public func deleteTravel(id: String) async throws { travels.removeAll { $0.id == id } } + + public func observeCachedTravels(status: TravelStatus) -> AsyncStream<[Travel]> { + AsyncStream { continuation in + let filtered = travels.filter { $0.status == status } + continuation.yield(filtered) + continuation.finish() + } + } } private extension MockTravelRepository { diff --git a/Domain/Sources/Repository/Travel/TravelRepositoryProtocol.swift b/Domain/Sources/Repository/Travel/TravelRepositoryProtocol.swift index 37247b38..4735c95a 100644 --- a/Domain/Sources/Repository/Travel/TravelRepositoryProtocol.swift +++ b/Domain/Sources/Repository/Travel/TravelRepositoryProtocol.swift @@ -13,4 +13,5 @@ public protocol TravelRepositoryProtocol { func updateTravel(id: String, input: UpdateTravelInput) async throws -> Travel func deleteTravel(id: String) async throws func fetchTravelDetail(id: String) async throws -> Travel + func observeCachedTravels(status: TravelStatus) -> AsyncStream<[Travel]> } diff --git a/Domain/Sources/UseCase/Travel/ObserveTravelCacheUseCase.swift b/Domain/Sources/UseCase/Travel/ObserveTravelCacheUseCase.swift new file mode 100644 index 00000000..12191f55 --- /dev/null +++ b/Domain/Sources/UseCase/Travel/ObserveTravelCacheUseCase.swift @@ -0,0 +1,46 @@ +// +// ObserveTravelCacheUseCase.swift +// Domain +// +// Created by 김민희 on 12/15/25. +// + +import Foundation +import Dependencies + +public protocol ObserveTravelCacheUseCaseProtocol { + func execute(status: TravelStatus) -> AsyncStream<[Travel]> +} + +public struct ObserveTravelCacheUseCase: ObserveTravelCacheUseCaseProtocol { + private let repository: TravelRepositoryProtocol + + public init(repository: TravelRepositoryProtocol) { + self.repository = repository + } + + public func execute(status: TravelStatus) -> AsyncStream<[Travel]> { + repository.observeCachedTravels(status: status) + } +} + +extension ObserveTravelCacheUseCase: DependencyKey { + public static var liveValue: ObserveTravelCacheUseCaseProtocol = { + ObserveTravelCacheUseCase(repository: MockTravelRepository()) + }() + + public static var previewValue: ObserveTravelCacheUseCaseProtocol = { + ObserveTravelCacheUseCase(repository: MockTravelRepository()) + }() + + public static var testValue: ObserveTravelCacheUseCaseProtocol = { + ObserveTravelCacheUseCase(repository: MockTravelRepository()) + }() +} + +public extension DependencyValues { + var observeTravelCacheUseCase: ObserveTravelCacheUseCaseProtocol { + get { self[ObserveTravelCacheUseCase.self] } + set { self[ObserveTravelCacheUseCase.self] = newValue } + } +} diff --git a/Features/Travel/Demo/Sources/TravelDemoApp.swift b/Features/Travel/Demo/Sources/TravelDemoApp.swift index 50c9c622..15605a00 100644 --- a/Features/Travel/Demo/Sources/TravelDemoApp.swift +++ b/Features/Travel/Demo/Sources/TravelDemoApp.swift @@ -19,12 +19,16 @@ struct TravelDemoApp: App { private static let mockCountriesRepo = MockCountriesRepository() private static let mockExchangeRateRepo = MockExchangeRateRepository() - private static let repo = TravelRepository(remote: TravelRemoteDataSource()) + private static let repo = TravelRepository( + remote: TravelRemoteDataSource(), + local: TravelLocalDataSource() + ) private let store = Store(initialState: TravelListFeature.State()) { TravelListFeature() } withDependencies: { $0.fetchTravelsUseCase = FetchTravelsUseCase(repository: repo) + $0.observeTravelCacheUseCase = ObserveTravelCacheUseCase(repository: repo) $0.createTravelUseCase = CreateTravelUseCase(repository: repo) } diff --git a/Features/Travel/Sources/Reducer/TravelListFeature.swift b/Features/Travel/Sources/Reducer/TravelListFeature.swift index fdfa27a8..34d2da05 100644 --- a/Features/Travel/Sources/Reducer/TravelListFeature.swift +++ b/Features/Travel/Sources/Reducer/TravelListFeature.swift @@ -16,6 +16,8 @@ public struct TravelListFeature { public struct State: Equatable, Hashable { var travels: [Travel] = [] var selectedTab: TravelTab = .ongoing + var cachedTravelsByTab: [TravelTab: [Travel]] = [:] + var didStartObservation = false var isMenuOpen = false @@ -44,8 +46,10 @@ public struct TravelListFeature { case refresh case fetch case fetchNextPageIfNeeded(currentItemID: String?) + case startObserveCache(TravelTab) + case cachedTravelsUpdated(tab: TravelTab, travels: [Travel]) - case fetchTravelsResponse(Result<[Travel], Error>) + case fetchTravelsResponse(tab: TravelTab, page: Int, Result<[Travel], Error>) case travelTabSelected(TravelTab) case travelSelected(travelId: String) case openInviteCode(String) @@ -66,42 +70,83 @@ public struct TravelListFeature { @Dependency(\.fetchTravelsUseCase) var fetchTravelsUseCase @Dependency(\.joinTravelUseCase) var joinTravelUseCase + @Dependency(\.observeTravelCacheUseCase) var observeTravelCacheUseCase public var body: some Reducer { Reduce { state, action in switch action { case .onAppear: - return .send(.refresh) + // 탭별 캐시 스트림을 한번만 구독하고 서버 데이터를 요청 + guard !state.didStartObservation else { + return .send(.refresh) + } + state.didStartObservation = true + return .run { send in + for tab in TravelTab.allCases { + await send(.startObserveCache(tab)) + } + await send(.refresh) + } + + case .startObserveCache(let tab): + return .run { [tab] send in + let stream = observeTravelCacheUseCase.execute(status: tab.status) + for await travels in stream { + await send(.cachedTravelsUpdated(tab: tab, travels: travels)) + } + } + .cancellable(id: TravelCacheObservationID(tab: tab), cancelInFlight: true) + + case .cachedTravelsUpdated(let tab, let travels): + state.cachedTravelsByTab[tab] = travels + if state.selectedTab == tab { + state.travels = travels + state.isLoading = false + } + return .none case .travelTabSelected(let newTab): + guard state.selectedTab != newTab else { return .none } state.selectedTab = newTab + state.page = 1 + state.hasNext = true + state.isLoadingNextPage = false + let cached = state.cachedTravelsByTab[newTab] + state.travels = cached ?? [] + state.isLoading = (cached == nil) return .send(.refresh) case .refresh: state.page = 1 state.hasNext = true - state.travels = [] + state.isLoadingNextPage = false + if state.travels.isEmpty { + state.isLoading = true + } return .send(.fetch) case .fetch: guard state.hasNext else { return .none } - if state.page == 1 { + let currentTab = state.selectedTab + let currentPage = state.page + state.uiError = nil + if currentPage == 1 { state.isLoading = true } else { state.isLoadingNextPage = true } let input = FetchTravelsInput( - page: state.page, - status: state.selectedTab.status + page: currentPage, + status: currentTab.status ) - return .run { send in + return .run { [currentTab, currentPage] send in do { let result = try await fetchTravelsUseCase.execute(input: input) - await send(.fetchTravelsResponse(.success(result))) + await send(.fetchTravelsResponse(tab: currentTab, page: currentPage, .success(result))) } catch { - await send(.fetchTravelsResponse(.failure(error))) + await send(.fetchTravelsResponse(tab: currentTab, page: currentPage, .failure(error))) } } @@ -114,34 +159,41 @@ public struct TravelListFeature { state.page += 1 return .send(.fetch) - case .fetchTravelsResponse(.success(let items)): - state.isLoading = false - state.isLoadingNextPage = false + case .fetchTravelsResponse(let tab, let page, .success(let items)): + if tab == state.selectedTab { + state.isLoading = false + state.isLoadingNextPage = false + } if items.isEmpty { - state.hasNext = false + if tab == state.selectedTab { + state.hasNext = false + if page == 1 { + state.travels = [] + } + } + if page == 1 { + state.cachedTravelsByTab[tab] = [] + } return .none } - if state.page == 1 { - state.travels = items - } else { - state.travels.append(contentsOf: items) - } + var existing = page == 1 ? [] : state.cachedTravelsByTab[tab] ?? [] + existing.append(contentsOf: items) + let deduped = deduplicate(existing) + state.cachedTravelsByTab[tab] = deduped - // 동일 여행이 중복 노출되지 않도록 ID 기준으로 정리 - var seen = Set() - state.travels = state.travels.filter { travel in - guard !seen.contains(travel.id) else { return false } - seen.insert(travel.id) - return true + if tab == state.selectedTab { + state.travels = deduped } return .none - case .fetchTravelsResponse(.failure(let error)): - state.isLoading = false - state.isLoadingNextPage = false - state.uiError = error.localizedDescription + case .fetchTravelsResponse(let tab, _, .failure(let error)): + if tab == state.selectedTab { + state.isLoading = false + state.isLoadingNextPage = false + state.uiError = error.localizedDescription + } return .none case .travelSelected: @@ -227,3 +279,17 @@ public struct TravelListFeature { } } } + +private struct TravelCacheObservationID: Hashable { + let tab: TravelTab +} + +// 서버에서 동일 데이터를 여러 번 수신하더라도 한 번만 노출되도록 ID 기준으로 정리 +private func deduplicate(_ travels: [Travel]) -> [Travel] { + var seen = Set() + return travels.filter { travel in + guard !seen.contains(travel.id) else { return false } + seen.insert(travel.id) + return true + } +} diff --git a/Features/Travel/Sources/View/Travels/TravelView.swift b/Features/Travel/Sources/View/Travels/TravelView.swift index e65a873e..1dd9bdc2 100644 --- a/Features/Travel/Sources/View/Travels/TravelView.swift +++ b/Features/Travel/Sources/View/Travels/TravelView.swift @@ -19,21 +19,24 @@ public struct TravelView: View { } public var body: some View { - ZStack { + let hasCacheForTab = store.cachedTravelsByTab[store.selectedTab] != nil + let shouldShowSkeleton = store.isLoading && !hasCacheForTab && store.travels.isEmpty + + return ZStack { Color.primary50.ignoresSafeArea() - - if store.isLoading { - DashboardSkeletonView() - } else { - VStack { - TravelListHeaderView { - store.send(.profileButtonTapped) - } - - TabBarView(selectedTab: $store.selectedTab.sending(\.travelTabSelected)) - .padding(.horizontal, 20) - if store.travels.isEmpty { + VStack { + TravelListHeaderView { + store.send(.profileButtonTapped) + } + + TabBarView(selectedTab: $store.selectedTab.sending(\.travelTabSelected)) + .padding(.horizontal, 20) + + Group { + if shouldShowSkeleton { + TravelListSkeletonView() + } else if store.travels.isEmpty { TravelEmptyView() } else { ScrollView { @@ -47,25 +50,25 @@ public struct TravelView: View { store.send(.travelSelected(travelId: travel.id)) } } - + if store.isLoadingNextPage { ProgressView().padding(.vertical, 20) } } .padding(16) - } + } .refreshable { - store.send(.refresh) + store.send(.refresh) } } } } } .task { - store.send(.onAppear) + store.send(.onAppear) } .overlay(alignment: .bottomTrailing) { - if !store.isLoading { + if !shouldShowSkeleton { ZStack(alignment: .bottomTrailing) { if store.isMenuOpen { TravelCreateMenuView( diff --git a/SseuDamApp/Sources/Application/LiveDependencies.swift b/SseuDamApp/Sources/Application/LiveDependencies.swift index 85f8103e..a7163d66 100644 --- a/SseuDamApp/Sources/Application/LiveDependencies.swift +++ b/SseuDamApp/Sources/Application/LiveDependencies.swift @@ -13,7 +13,10 @@ import Domain public enum LiveDependencies { @MainActor public static func register(_ dependencies: inout DependencyValues) { // Repository 인스턴스 생성 (재사용) - let travelRepository = TravelRepository(remote: TravelRemoteDataSource()) + let travelRepository = TravelRepository( + remote: TravelRemoteDataSource(), + local: TravelLocalDataSource() + ) let expenseRepository = ExpenseRepository( remote: ExpenseRemoteDataSource(), local: ExpenseLocalDataSource() @@ -45,6 +48,7 @@ public enum LiveDependencies { // Travel dependencies.fetchTravelsUseCase = FetchTravelsUseCase(repository: travelRepository) + dependencies.observeTravelCacheUseCase = ObserveTravelCacheUseCase(repository: travelRepository) dependencies.createTravelUseCase = CreateTravelUseCase(repository: travelRepository) dependencies.fetchTravelDetailUseCase = FetchTravelDetailUseCase(repository: travelRepository) dependencies.updateTravelUseCase = UpdateTravelUseCase(repository: travelRepository) From 3cdacc1c33d0492fde04f9de1c618604ad37a1c7 Mon Sep 17 00:00:00 2001 From: minneee Date: Mon, 15 Dec 2025 19:50:12 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[chore]=20EmptyTravelList=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4 \354\230\244\355\233\204 02_55_30 1.png" | Bin 0 -> 17285 bytes .../EmptyTravelList.imageset/Contents.json | 23 ++++++++++++++++++ .../EmptyTravelList.imageset/travelList2.png | Bin 0 -> 59150 bytes .../EmptyTravelList.imageset/travelList3.png | Bin 0 -> 124768 bytes .../Travels/Components/TravelEmptyView.swift | 2 +- 5 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 "DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/ChatGPT Image 2025\353\205\204 12\354\233\224 12\354\235\274 \354\230\244\355\233\204 02_55_30 1.png" create mode 100644 DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/Contents.json create mode 100644 DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/travelList2.png create mode 100644 DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/travelList3.png diff --git "a/DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/ChatGPT Image 2025\353\205\204 12\354\233\224 12\354\235\274 \354\230\244\355\233\204 02_55_30 1.png" "b/DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/ChatGPT Image 2025\353\205\204 12\354\233\224 12\354\235\274 \354\230\244\355\233\204 02_55_30 1.png" new file mode 100644 index 0000000000000000000000000000000000000000..9b416ba505ac7f1278e26a62f1b5c99a2e962854 GIT binary patch literal 17285 zcmb4JV{;`8&#rCT+VZqLwQbwBZQHiFb+_Hxwz;)!yM6EHC%iMkRX1!dfm*YGzdb|u%;Tj@@;H$&2`Cus z7@F`uGt7QNO-#}JR}~B|dzb{8=wg13O-Kwcm;?|qS{ND#P;!}n$x^~pAM9Cv9ltZq zwXeSld@i=n&*gmu3Mf*tb^>Oo)2`3TsyeP;@*nsn`5*yQCM^FCxsjI~KG@#IPw^my ziuG3#G>pM!VxR3ibz!xEnlj;r<>9r)DW9oGpSA{%dum@@t>1Y)pEl^Ut^Vv9=_PMv zexo)6&T)WHj)B0>DL#Jpyc`rppI z$ItBW-{10a2)uUYv<~7pervGcftHL*yzl=#VeS!NdZei{heJ1UdUM-5-GrZ(H{3hyfyOvc z=a6zQ0We7;VxVD42Az=-zWon@d*T#^ePSS(^t07c=N)EG0>1Zah%tKsP;!PnSWNob zZm*Xj@}1|&$hu8#7SFHY@2ads)W=G52TFzNpw8{V-FAcJJ--8fPU~;Hc3%uEGd^&Z zvu&F|!~@F?t~TK^#1hUPV5`Z9f^AXn0+{I4Z%C5Pb z>#hGozPI998po^<_biaq!`JT3N`~KaVKKytUsLoT7m}i1O*brttX((tmvZ^|4><%a zjTIr=9D(kqQZ|b?4CRv>-XUBM)&U8sb{ugIK1Of>=pB&~!7mWmFenv6QHJA?#=s7Q z0}(g1cl_VI3OsP7SYb*e`{DoIKC=?bkU9($MRT86`N8E4Z zWhE(cHUD@i7E+;6p?;=VFc@eA>JIXSvK z|6Mvdh`2k@vlKL3&v9j1MS<&RuJ`4!gORl7WTK4=uG$yo4#v!%q^V(HmyVIVUkr>N z{X2I-y~)@Gd|w(P3URCkLIwm)9SMg?v?GnCwrpP_`%p~x=<2%sazdu}6&z z6xgfS>apqM@l@{7aU4s`;jo}}!&k+SH`6@BX?ZWa7--_|=4B6zeiu86AQ=)$okVbU zJd~&FDsH@q43Xi`t**#~1q-WU#qc2h+R8g5QQ5A)QxxH+qYF;pVKkDdC=MTS`G}2dHU};dlu2l zE;du(k@8WDk;ILLX&eS*rFN0^V4Fqzl7WLJz*@Wvg#40}_(d8uk4z+VXWSmPC(9S|T6My3702arX3t?C6-YdPOJ z^%7o&hjoJ^OmWWkr>eS8D8CCEhQAU|T#oh57lYqcKfQx+J%$W^kKH%#Aby?bj zuM7Ms022>9iZa`<=d#}$s;|+zY7|{4G!KRFD*SVe%)==B8Ka&3^nHo~S!$^Jxhexr zv`=!5n0SZIPjgBIkDWVfIRPey$tf0eqPEXYap@Koik zW-hsP&%jm8kP<&RM1n+ASI@NcKpXaQvpMI;1vitoGP{(uNBS4(AaZ?$qij|>EV3^b zN~pLoCZ431A!r7f*-dpXcy;A7DXg2;xR@NRF#&UeuixAK5rTN(B{E_OlU@f#-Op)l zSS^nw1=_qNP#gjCNcC@2*rN}~@p;q93Fr1JLC#RONy;+4r!i4`zqqN5L~jC@AHGN z|Ndcv6Pob%!~Y4Su!X!_DmbxQknva2Im;DTnP&XWmZGKg62!^=wwwkst6fj$Y=`~kzKR@m z3pzUbF>zsv%z4ggJQY9yj~Y})87CB6S_~1B*`TRDVZ_~`f3eanrSIj&lf&&+%o&bjai-s}K$G#`_S19?`Fnt{dHbegXlaRWW0?t8 z(EZKMYl}8JUEe$>$#oOF`#YkGo~k~I+ACES^ET?}ju*?nEx^v5%HJ3}R&XsNh{yY5 zx;}qrLndBCdlKi@9h{VC)*<kfXcSI>tz*L_{9w6irO_an$_h=S3)P)ll2i3T1jIzzb5vv8rzqqN$T!n#%y!(imk zz><{(F9_v7t3Njk{k`-bZ~)!TKh<^FKO3&;_~_#XT9%5YR2dH-xWCNcfcaH~chWZx zHkWB+O~!!15PSsjNSxoS2;T4REY*sE96Q?JbUT`mTuXo!J97VDxD)GTuQ4ofELme= z^C+W+l-ORnf1wIGk4zk=)ijbSQ?d)&`6>A~mVilLhu9vC=d@@`3l$6~R`&z^ZE&)% zeK>uu_Zc+MvIxW`h0U=s?B8cfBRx(VJOV%a*u*)Q>qp;?%HJA|^|Hw?WwQ()t{%ip zd5`DETrZz{*K*Mrd`0NS|FIhXTJ`t-DquW-O-LapCx0e(TqB| z+1ni%pWsjjVDUSsvkaQ;l2X={p^z_td+P3?01P&81|ZP*3`{Q*wV?f$mMr`q!I>Ji z1wQX;>o>hWHhK0yR>5!J`y%f;P4A%XuLui)c+0?P4&niNFP87ZP4zp(x1P~wh-9+< z{*y!EpbLbfE+CHXgax&c8(S?PQ%VLbt{g%Q+l*UNJ!x9Nn(mbWB!-n&0=Pld=E~j8w-p3FHgZ^jS;B8TQ zVF*n-l*+RC0Qs#9HtY#?mFc{S+Nbq&qC>W(A_1j=NJL$COz;-&Gr>`%V84qMb%yhsG;v%d z#d>5hfiKe!%l8k$hw_OZD31G6ScdK*AIM$@@&G8>QX5OVZ5nh12F@l5O(rtHf3F+G zaA>B8fZnW*HcS~d=#U>?o-;bv<5=TgdTn*mU)TP+nWwq+MCv~SdBiO-JhF( zJVOZFJxdG*Dl_hdjWTFHH$<2|w8MJ~d$7TjQDD;(c-0Ta5jbYKFk={NYgwFcdSgos zaV~;hAPt++m==URh@&F4H6al6ImsTx{Qt<)Av&U=7`ZuK+Q0!ydXQU7t z@I*>~Eu?Am3*1$t$5|?S(AJJ7l>e-U3C~q?VJl|CI0kzeX>4kHpplnc(YES=QP~9A1*}}tpnq7 z2-kRgB57GkJFVh7@}zg(H}k>Bq2w!8BapHg9PSCPM(VA;z#>$d`JRyl9tN)Q)mRvg zWkMi6>J6n8`$*&kq<_C5Zmgr}9iZ2A>+?uG(K6vdws8H7%6pz->N~%Ne$?Fky5WJe ze?4@@TDCB8yaKdc?)N5M)bZileg<_A!9B3hw69{8|2kZQgfyDSmF^3X6{4ZTwcIW| zh2s$U7I$Tev==uIIH0?5fFh><;M0Go`+7$<@bz<1vp%*Us6>g1b+2mtSBQr=0b5v# zBpjTpBB@S{Nu({%j3x%CFelCsP7y8 zNR@2_6XK0!-l;hs)UrkB==*XpCa6+6_l?w*TL6d1nnO}x0`M{>=eoK4yZfjz8>|J* z5vc+&#kKMruiO6>t0r1zr0_tWxZjI%>$LA24e6B7iicv7Wl-XSfDHqm~>TN(C{z$C3SHL0lMkvL8& zBbf;$_R>zs4~lGqGZ6(8IqLE*mmOBt`)DUAzde}fZ-vmPGeq0Ka!z^0sU)KS5L9v6Y?0iV-+H^_&m?sxaGbp4RDbt#q2&YdsEk zMp5$ZPlO-5El!>jLzJw6akVC8)G@M|VC1a)!?TA>yEb`x$?RAIRAD>ppyPqP2zcen z?7o6z;Yv0e4~0T1kL5TtK5SoywB1ksJ3df3X2H835|SE(uPUmME8P~6rjE4Gcu2+Y zW1GcQt2tdcqj4o;g_UN3&emUaM9W*CwvqTYaztG9MRqB%Mon?{_Iqt6kLMp}aY~^e zt){kgp>1A5c^Z>3vTUz68aqQNaCag^X;s%RJ!ygEUm%Cz@RWJki|S^Wf&#WcyA?{I z3pN!B^aBE`9L1XMaC~+kktc-oq!0xYyda53k*=>WZG72x#ci_YX{VWj@EDeWlvuM= zVeOv;QS}1J=KUMg7kRBc`yahzy{Hd#{0NtOh}JESN?KL2Irg9xT!a``<97hPVX56aouYqs!-_GlY?Pv*^T!e$jkTWDNb#t?RXlmlQhPhZ!2VLmC1~bim?utlL!D zr(BU4L}WHa^a!x>=iw|d27>-9*hVz=lf#+PP9l!tS^mmQ=flJ^q7Bi0C; zR6YEHSD&~EZ&t*`F`Ttgfg*qsIv{h4yT=V&ocX-v{I9+Ba22r$TY$X{HK(x-A~(Va zRD5(le$C8!r`+U~y#!E8T2+Q>-x*p#bnm<{RTqHI3(LbG3A`+PDioEjEm2^s8bOHK zrNC#$xO?~(G zQj!F*pZVyD-BQ0{7;G1Hojd?{JaTdzz#=(=z7z-m7kR_+LFo#TUhI<=)m%=eVS+7f z-G{aVJBj!q^p))T6VvP+HV22+9~W4MEeaF zBz?3x0LqK^o~P>1D49V+GC^TOIX856@G$NAhxosYCbRWlM?7{qi(Fbgq~xSo38Bwo z$yusJYl*SUD{0tbZ-~&EEAp=8Ttb(*QfE~$&yLszs))p%%wg9ZR4vQ<9$;*+y!OBs z(YX0Tfs$EX_q`W`fSN4f6J69bMlriV?UZm?nXr+4mBjIP5(WQN__R>Z3DNO;_51>v zA&_m)1LP7Yz^BD|!E5YY;nhE40s(oBt6~wmLty&w#~up}hTlVJTkMBGCIwc$7>HR{ zc$h(mA|j%oVfKVGqjkVNAIuH5^f5KfKQNl<*F-9~sZ{LC7zip*L*yGKK*%@OpT>i? ztW%aAurxxl&yM0kO@2va&$1cRDfp?R$RM^>d0)i;s-S`h?q96cd#y4bLFBbN8ZpC7 zbADg`1A|8kM6f5|Wf8{J{R6PLO~1fg3+9SqJV{E&o7ugO;`w|7#j8) zhC}tp;#c$|-$B8EZomgIgBV5CqYO(O7N&pz?4@)5ARnrah=)Agz&f%CK+Nx*+ zGYYzWz%NT)*SAdrt~ARIyCfW>1Kb>)&t1-h0B_D}a;uiSP}=m@8cAZYipX5u81yPZ z{PVlntp^=Kd--I#s3?HokSW+%M}0lHILZ>o|{c3H4P2} zMH)|e-nyLAaQHbKsMII_ly)jLs=lS<(}@~KvExFFstjn25W-SiPdDPVlHU5>9{@J& zSK#f#EphDCo4(d(1=uO+gSs>$ZCs_g>%#8gmoWg!Clo&w%e_=eBa{QnY;I%qu7sz@ z<(Aq|VB#A}_LY$z;!zPF;e`N~ND)-GGnGmHpK{NGL^Ft}@%Wq-odK%ZdyqVv!mLX(D0=2l3_%BIza_V0wufl0Gq7GR9NDW$kqiCEkP*97B_2r{$0TY^*xN9uFB`{#j;a02agdMeHaEw~ zZX8o{xPd7miJLw(-^bk)dtStwLl|Knw0e%vJpiGh zFRim2j#q0`43fJ;I%;-g5jZgXA@Df(oG~6^%ZG6ybK6ublta@{J|=CvcTqVOw<70w z)JmhFglemm%f1UK@IThY3Fj#zh%Cz(Dv|>qC1~Ci@l_9}tV!33d~HAh#ayjhuqndC z){L+5rk7C_K69}iftTwS{%c@YH0yQW%;Eq^{FdD=g8YyX{I5|%k2;tti++`=>ZocC zMZV&zita!;e)Bg)Qx$SPNh_S8{djje7eLyrN}(`|>HG9srXbiT5!`upbZmt%fbQH= zuI;>XY7#<*$r7?M?K~vh4&|;F{@gFH@zhE)GdIF#c`Lg9PqI5qJ~%{P z?;=e4V7#E?(3d;m8(7~QieP|1Se3P0mwcH!LIlF+#{L)!B$KGV)|svr|xIj4R1HAeSSE)=&iP#w{(ZTzB>w4Z;%H@;{qra zMZc;ktFFc%Ezw|@lo^>&(dOYF>LJ>u)m@T4f+hOq=6eUANQfWEM(aaaEjs~_984{0 z!6}SIwVL|%3u;r5Yz3Yz++6W)Q2L~%4!&HwYaOqAw0Pa7pe^b0>RbLp4oQ^*grRn{ zz`*rWiUXnC2?5wk;+rs4%ff*$!P=xx<1{+K+LA^^3DW_ZO4ii$=m+xH!ley_I$(p{nFe$`Uc-v5aG6)~%oMf$Co%Aj$jTv--lI<0_u zDK6H*h3qg~NQ;25VBtO7roH$3q(duE#((FqERw})gvYfCX-O{6bgN4NGNzHOK*QV zaoZ3@?Fhs}hM-Znyp~@~`7lgrf!_C!RRXU+k^(5g2caTTrj#(V`K*4;mhXYcu$<1X zY?o<8>QY@RxroNJpie&|N?(ttF&t6AZJOHstmG|oWwWYj zqQFViViY>=RHhVIZK@C}Fm*2%(vTW$OK;2TU(a6{73yU{mu{%2Py-a?d(LEM?Mp1BTvy zGx_=rnj%c>SIKu6&|jr$i&{x_cxD9v%b3{3q z67T()hqPLfU#@~{Rl-tCr3{vuSf~Px1}|_X?)5-;*Hg!8To{d7%QA&#=t}I<6G9ol ze1e$$xac*sEdJy?@vDPmC&|)EErhHF(my9V7dd^3gu2xq0qX2m*W)@;MZHWzrd6^j z8C72}Q7k929Gx4{NzOsKB&Sdxki%-O!D-m6_x!C!dpGB3hN?iX#&)!*jC`OVD^nFG9)x4taC`D!d~CU1{%l)1T01-YAyw8%1Wq| zlK)B%RU=~6P5;$~wi=!-^IMM9{ii@72^JuIZNaAuGAP!7c_UM>_4f3>^vdLTu_(}Y z6E_wa5qZ}yQaain9PKRt4`Y1(9!T1g-Y?+>t+`g77e%?i3H1A-(^~OPoyuiL?ziZB ztUQ57j+C%vlPX=V61ER-6zMuEzERHM4q?Oh5IVW$wXv_DgsNhNK=8*78t6B+220GA z2b05GIt-lp!CA@!3bC#<{k+5$>~xyK$AbNF&kG>hl)@@g8rHYC zOeX7jg043!Gcq;@m9<-QCQn>j8?Cz4c9H$KHn<$7A#6{@RABpau)O_=O&rL2$OS{j zI>u=2&j8lTGg3G6+vuvCxM6)ssMNs_KeuJ~B=biK_PU=9g+qduDRtbd!fH+=dhK@V zUZFo+ZgOvvb-T|5ktC2)k*q=wVY?c#IVXM-mSa0A#-Y>{N(|jqAEI6*TN>4F;_>sQ zzN6#Z7pK$V>2|CiddK~Hlbrvjvp5gc4TZgfh~2<&3T>|8IiKq{J@*%%1Toji^|X%O zqrM+nEhmb@5`kDwbM=G6pY8`HdIQ@CV^Sfk-W#GNnmQccZ(RHkYbxu+B3&Nd@53@l zMT>_1Z${?@Smdaew*i>CWgtX6vFX9qEE_Bi!%}!G)9HzMGMC*!)Y{fY>t(zHn?6Fx zssFOHID;YMm*L5Vs0=~)UtI`EcpO+djF_odcCTKa(A%KEgZNIHkuvN6Q#0?xawsTI zScb(fZ>{b4Wz0Mn6IAeZj_mq~rnMCpHn6bvuRo`=3HU#>-BenA{9~5}D$_MubxJ&0Rd=&X>b?J0TB| zg23BShC>1O+u};D$brS}*>wkP$3Uk?r=>jsV2omORHmkqSxn#8jqgH+I?C*+|J*f1=NW(BStLTp<;5}3>+x04J^P{qk1)vztjO;3f;7h8MM%LMp z|B97rJZOwLxYb+Nd(fdk-hbtF7E)>Iel)~Bjkjan*S3NKWE(LWDbGQeDo1Hp0yBPpT8>|VE0JMxBYxyyfN6dsS~Fe?y=CyEzH^* z+jNitVkB0)cqv$i=8l@PB#Iw<3{aQR8d!YwSNsn6ZrUruuq+Z-r-lBOaV6I};Bipv zd47}Xc>tJPp`b3Q3njwVgd|;iUk7aGUCiDeSsi$6(*DFcH`UsVTMm&y^4McfbEbLMWKx9f4GQXllWMqD;`Hd3c?rYL2U~A(z3nuP; zsT}}(!~+5~Ps=?7<)(#9pKa$hHXs3(3JN|gB?ob%$T>VOn`f*$E=Ma2_yR2^qj0f^ z8Pc}L2HyuE)xQbdv<~`R1T=X_y%X6WC`Kna3|s(CXU0gCFm8zSN~@;qvK^00?M`4E z-eQI}30jAFuA^=NIv>Xe*d7A6^Njy^uz3nuvOz+!DVh_hLA;bwY3&?`oZ*GNeF}aU zyWpe5i^c4>k+Qt-yv~+K*J`pPlGSX80;SM@M3^uFcbXQ{iI+P%R(wAT_dg#>63v^v z?^xroeP1M|(UJK^d5$Y4e-Gg+ELD+a^}7ySUm!=gAR2X+zC$?8kf1M98Z-UE)KjV z-yQ;AYPD4FYSB?reKqb($NB6%`@&TdVdYqeNZgtc>)hb4S2moIuj7~x-_f8FSgKrQ z*+$#$1sL8H8!eVAE^5~)DnH+ge_Q%tlIOv2Ktn|=6a?&O60jnJ%hN4xCAOyOF3!E* z^|o3v7IF_<$e>)1xN^R{-hN0U2v~?)u3q7nR^*pNPK3;JpT;H6vu}u_tRse)p*a7* zi@I&SPb1uW2LCR=i>Ru~q(Q>Q^oAtHuwE{{O2aB^PYXNg!Vp=FC6%>OCH&de*!w*P zK`uENi?`r20NXkxUQZKrP*Rx|;0vl*6d00v^+nkwGY<^e34$BADCOb$>5Sersr7HAB}Bst z4m*n))g%gHsr1-{qFwO!H0NQ-=hOHJ!`es(uQeuHExTA@^#-JV(P&77+Y<)82D%d| zxvJIgCdoMzSm|tvPw%-1s;2c68x=nDCV5TANmPKaDG(%l({Zriyt?}7*Qe|9gpk`{ z<+M%ZsNLpzU704a&TAPPWcqW3{ke9}X-;;36Loa+ zJr^>9>GqeT@88#aO6--&&iS-vHh9beI*_!L+1G?%nsM!HV`4M(nV(N45L*0r0vue# zK%*QE2YxwLnOYv+af4Usay=gXy)v+mL*eW5tF3aVxO&v+5#>@Ut_-gv&Zt>#iesg? zSF@a@7suOP+aQTNwlPxW*9Xw;+1clAA`u1~B^gtY&u8@L9_fO3u+*mwl@8^aTHz%z zykv=4&Wm_<9PlXtYHS1FVYRHLUTqRfv71+o`rju)L|a?brGNPC_{-F(?t>-~-gM2o zO`?|}T0~%|>)ESNdZGhcqbAIG0nGQug}t+Evw|zoagoSQ_Y;*Yzd(f;j?(yEz~o8- zDcF)<8ya!)IQ?QNXS%N~0ARdjN}xChg9T&Nmudmp5=hqU7H+jVFB|;-IE1g?FXUd- zhe=_on2{|waG@)shy%&EuXv$STVJC=p`ln2_NBmCy0u0!3LI}}6LzH@8s&Q^!bbfo zk#Wj^19p-F<#BKIj(yFzJy4(=fO!xIP7lcQ789hl_WRJS7Sa9cdQ0)BRa?f zJ?Q{K|$_{w|VIK_#Pr6(mBX=o@F+T>V5M)2$<#<%sMwdLNz@bnTzj*X1qZ0|j z-Eva;jq*dccxlYESKRfE7q-xBp-(IH#R~WB@rBo^rj5v~JoY=!-^A<72-_W!Z*h34 z*&5V!`N2?|{lxLr{^r$eN03pwAqnq;yA_$rOE01BWUlDp+xZR)$V(m4Sze#ZMR@rM zzJrdt#TcZrB@^vtyT7%~c+AXQH@FDJ%gkf8*PwI;kmK(ZrtZkXja5YZuipPL5B7z} z{hG2wde?1dHn`W;tX=E)?3{Bu7sYGjU_$Iw6!73&U6Q<7B_a<)5Wv{#5Kn1nGVJSz z9u}OhWnNhcc{%^j_vzb*n?BIwq#C18vI3UcBbe|tOa1<vZLkz4&It~7H*bjBi z5`aTDo0mB7yvyoTl#W|Wo8vT@3=Idm#K9M2SZ}Be7q^LYSrrnJf_{x4Bs|Iuu*62P zeqg~6pu&N96u9@qv#~SY*Vh8dzO*%3kr&kAL^Y)~2R53=1A3hQ4-Va;bdF~+Xae67 z7F(wgYqq4x?R>9w02JXc&b03jrA!Y`*s8RX^XUQzy)TNS#_BVN(GLuV=kaPUE?;AS z<6`}V3T#|Q=*pvsF0#VFppq+U`yJjA{T6O4tQU&Z2#G-AXv|r?`y)N_G`8US{UX>W znM>a-kth62MaZCnH3hXkAZSXU8Pl9V8@w_#>TiDn6C|NP5k?FsTH8N7+V%4&i?3HO zPQkNaaKy|X<~l%+-CnS*ysl`{FeWp~XhLzF!Rj5%r2uMS`%Mon@%vzbJ@3CbjGf#( zd28I<*Sbj@hecG8zBZgQxJ)7A$lk%vF}$RY!+kwAY8l zLUwYZ1VOD|g;k_tmSv{5sj2HpW8dt%NF7Z_ht)Tsji)oW<3V7t9io@FtlxZGd)Z}n zQ6aO$0<+YvBs(GN;T`ay?yy@ZDcF7wm{A87&wPj<8l0A69d0{ow+6^9Q#wy8CQ9Tc z&#*&^4HQVSbz1L@r_aUq%n0GTq|8&wK9La1oROXX(wA5^8&9?W%<$k`p{P$#Vhou` zPvT^oig>vou23HCB44Rkdjp7WW`$nb=$c^!9ebgrBb3Y=wL&6rg`|5QgI1G23_?4+m}4qY z61dqV_X5kdByC3FZCqiaxvs154goF$Qh=%9>!XEtb8=BO+p#$it zBRZKUCs%`8(i3X}Glo8HqfQCvp0>fC zXh43WcJ7!os9d)rhI8owas%(uH@EMSdSd-U<=x~#f&QQf%K;jCxr()t3`aI;h_5~N zgCk9G{ER`e`F*6&)ISi=;<5SEGapGOXCb;dw=V3=B%$;;D$wkzMOO{X<;k&3oy7+K#2x&}F8w^jHE>}T z%oak%C@G>J#4a)``_M`?muQyDI0EtQkTa#?MIDZ`WJZpwh*Ix*9+Clk4op|FzM#(< zSXrIe!%)tW+&ClVMGse+M9?Yqm{^!@@XgrJ1+q=Oq9LISbn=B=wUcXrdKUG3 z_zQ4CG33KKP$$|dTX)~D>aY(AsGZRI!mEFena!T~e};UL%Tr99jSI}wMkoKv9h!;l ztj!TtlT&TNiz-hxygJ9yrkYnAWZ~(QqQDHP8o@C2c_(G7!;(vZ;`)0?X=SuZqbm!E z0u)4ZP)VCN7fy+K@mvJ;6^?+x`8&c8Zm|oCLxn5;->-H}P7N=D8CkZGH53%(J}kI0 zPDC8r zP%z&>v)A`aHk&c$Z_v;B%1XkQ4jgaqmBjLL5!@e9g<0E$MB z$*$QS`v|f1#BMkF@H~OX-!8Y+^G<|&YGiGv2aK(6sL@1AJ^XP*xdyQ%(jgDaY|ge{Wbgm)CD6t=;72Pk?gToIp5l`W=4H$Npc=S~5*WdMV#o*$u7@hyK}i}U)kWhl9j_iTOY=jvcj^3^$IL9AevZvr z1?{~r{*idi%|>zNicTjCBzJ%`FU!}i88xlJY7t_`Hv}9XmAh$h*wTP4gT)jF^IZyN-^;vWI_o`;9W&!U&*qg=~!HHMZOSesjM5e%0#@+`bU+6fV+#~D!#~j^&2xr_Mxjw+)cPbokV}0(ku3!r={GB5gk7U#dml? z_ZyFBfffV7-*R=S3FeSZo&J>ZfS1nds5#1_P5f(o>iDBC6Ant%S<>DGCys8POZoN& z8Km*7D5juH?#x*}Tx!BtDO-Cpy3K(nfr>+e zLToC~Wv*SUPD}SQ|KNp#G#6g1sG~Wo>w6lawV9xHP+gTxH;}Z8RY*l8VI<1*Av=QY zAQ67LL}j}=p!a8}o?Vv9N+gIa52E25V$Vr$4^464mTZlaCK$dW^%l#7NJu4d=lNez z<@pH%PR+bt47kjyV$SV_c?V=+g%OTJrP3}Uk`g#BYS+pbh2^{n)v=G*9(olp&cFiN z_;u;1RUWmK$K??$lknHa-!BYM;b_h=-=LhhK1XKF6HLN#%C~p! zf=HY(y893-z&`4X1P;UcP@iU88dKyuLYM&w3E2%B-j!>sT$_}`k2-UFsaN?9m!j+y;3cX#nHMHP+Up07{4eu5lPqKWm~;o%b#D+KJh_zbRRWoln!ez3$qj4*oh!LurwI>KzBN^P(c@Qf7lE5OXCZ%$@eCuGXxY>+<}4s#24ZjXJ(o!|k!EDZ~@ zm54c)4hu1+w!ikvw@%Nbti#&Sjr_(~vdqo(c%n5Z_W@XYaWqP!5{kb?hcJeAe;KJ~ zXpEU{9j1o#N5)aduVTA!>;G;jk7AB~yJ|~|TM#A1ios5iif~2|C6mI6L;B;{g;)Oq z^{=H?TK7e>MxIw)Bf&yMq%(NORt}$J*p|+U4k3x*^`*_2a9r)h`VF{jvq@0|OKp6| z?GMphekMCx`hI@lLurr+wl(ZJz?#B#^Z8VrGa3L)VV7d8yo;~))PnYe>IuM?WnC?b z>Bi@dC|0!OjAv_YCKDJhTp5dGlOO@5^3J>+nL|Zj5G^6=A9J`AL!0h$1=}Q?Wi2`2 zC}rRd6flj}G*Zm@hv9QSPVLl4PkB?$G4mrHJ}I*=5J|5+1llxwdgHJ(eOw!!k{~vZ zCz|*>+kRyLHHcG#V~n^h1y1$_h%UQP)F3SEf{HWk1tCg=CFg`)bfq6%pttEylf~zQ zsMDcJSVfrdgreWY6yd*7%W^}P~1JNaOH2)1_p5ni7;CU30DI&LKY_uA)a0W5_! z3Er^epEUF89c{U7mb%b#To*ypyd%5gPo60yfxJ?QAUw9*e^ve_ifqP3J&OitJ6=ku!68L;ji$r|Edf=8rE*km&;mGw|Z zjm^>a4ll1|@_pj@ul$gQybVgI7YvfGvLKiN!`cQ0qbaw$Mo`9A8%%@m>$Az)vCEEe z!mbqm#4#O<%Z}XL7hOMyjYaUkZB8Z)yzSpZy_kf%&LFPkM)#M8M49N&n7bo!7vV*~ zPh2lVCYfW*Mi^En+2ow?!_~d$W_e~0if+E#rp3Vw#iPe;`q}oJi>z8~vy6d2QQ)L@ zJ*KgocMS+Pp7Pcb@8Aj6k0rfwXKil&P>^ieIYVZMRJ&8U$9i&&(dprapmPM8*Rm+{!bvSmSTdtb|F zY#l!gvRqJ$&Xwn}!w=s82laHo=Iku6>WU0`6cn<(IG83ukT4uT4C}ZWB@X|Kc!jJL zk5#`H$@mP1*EQg{yVUh>puyd3B)El4&pk!Ae+YwvZI}^i0hh&5_aM)8d4NdsGwD2f zO+16T^^|ynsGR;zG#i5xykn?GO{QbS67sB8x2?{&bHsGl``wX8y71#4|3A-RMyjX< zuE>rr))pZ#MslpBQz@O=jUnE~ zzK9iJu8pr-UUkH_yQ}RZj&QvvpRB?d(EigZd$}lM@_!fAL${sGn^&hbH*eS077A#p zChA$!GBlcvpfQMgMGLKxn~7;P*NtnOb_<5(gh}E_)}m$6uKo=lMBMOFDh$jaP&A5) z3i-5$#tQ+2JlN3?EFP*XqPf)YVvcJRF#Lldgeo1m3iw>5t>85^HL+#Oh6gB0#sKrL z3n|rBGFY_eWMSSnY-lp;C#9=%U6^!W&SCXc&0nx=yOq-1>U=))JyZqtx@Pj~WXmKZ zkN&a4!J+F#*TldN<`lgw&|-qd?V%yhJLC|%=mTT+es`Jw85sl2Fv`4n^UU+O+l$iP z-j4C3*=#9>4$7Ai2sxmj7?Uv>lQ9{SF&UFF8Iv&?lQ9{SF&UFF8Iv&?lQ9{SF&UFF bk@Eimmidiu>;E@y00000NkvXXu0mjf4v;3w literal 0 HcmV?d00001 diff --git a/DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/Contents.json b/DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/Contents.json new file mode 100644 index 00000000..22114761 --- /dev/null +++ b/DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "ChatGPT Image 2025년 12월 12일 오후 02_55_30 1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "travelList2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "travelList3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/travelList2.png b/DesignSystem/Resources/Assets.xcassets/Images/EmptyTravelList.imageset/travelList2.png new file mode 100644 index 0000000000000000000000000000000000000000..276766c70d3a89d64e0ca3440486beb039cc321d GIT binary patch literal 59150 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 z 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%0AQKLB?|#%Qh_`%VT|N2 ztx-0Fnkhu;WR2CuGh%S_uBUZz1kR z$BBrZmG)VjPVS~E%Np(q4hp2qnQha`_v~UeAd>6OcNL{Jc^y zk6vY1GIdWc-;0eqtPV48aeLU(6LRWuc!_Ap@)@fAdc3#1c>~&o9wx?$A-Yi=4#l(# zf3ZyAW!4WA5D+T2YvBeWVtKJtRwnf57YuaH9^*ca(|&L1NaobiH0KwsrD!`YsMswq zZ6TLPFl!A#0aIzk(rjmtHgdkAIi_pj1V(w*QfmCtSc2f{K_wB_r%DqB0kLP!P>>tb zA#9jY(`5r`Dv>9Lcrnboa?^6CEzHQM2$F-$n}+8Re#>hFlUZtvj~}7N}v` zpc-#KLkS&1G97zHs;d|?4~o^>E*^a*^*|Ajdu)a;|JxFk7bVTP%wKH znd>nt&~>Q8VQYohHk*Vx$CM1f^DY#ug}zqVhLpEUOM_|Sl-Zs!G*^>izmWO+kj$24 zkXRhuLNP=a%NJMkW^`Fq+IM^pf{caDxEa`{pn^8b5&0_nqx>~ef#R*a(#e8r2lGOI zC$CPy4a!9p?z;Y2fLgd1;lu{rV}~<1V<4eU8}45!eMy=Dm3Dv(Z}x z8&HlG$d{qhb>Yy1rN6Sx520k+w(+02f>KRD4j$1C8RBa)2kNWS;i$R75%-}xUG@!K z2sj`&m6jFAM3Ng9a~R;ps2OC@JZ}x(YuZdGN=&{S z19E0Ra2DGsDON19kJQJyx#Uw$1J9PeAZKN(DTSAAmyugAlO)y++Zbs!hA6le&~zSX zLw&z92c;P&CZN`bTrZ(YT73Pqu z+ed+)S^{?Lgy3c-e)U~ylxnFHsUatN@ z(n!&eYh8N^bW3;H?&oEkl`QrwS`E=g%$t`s)HMESwwLCq&*6bO zIjY8tludET2+RSDa9?sENgD~#L1xY16E2{M)kbdN$nWW@BdkfWs#7VTlN^zp%3TF@ z-PzY_drrrG+fjmQ(sv%hkuqnVCxZfgr_rS_okqnPK-!qO^y?he2#|1a!bF~6%b?Pz}MH|Kz}vW)#qh#{nC|l79q%F~kyvM@9|ZynG@Y4t_~#&O*vfy6*1G zb%#$ZsPzCz9?zc+-|zg!x7?0e(Usa{I*?2pc2G{3R*vx>Al|b{hts^P4+#HOUzbT` z84)mL02c!1sK13np@;yZ;p)L;OJ&C2Qy*1DN_9*%V}<|u#=!Pgx2_XDl3Phg0D6pI z-Jo<4_@WIB42DLGHXnk@@i+?eXcBm(4wam$I|RZPvUIiut$wj&CYeK`*-~bJ<@4!_ z)UNg;ulpVM;be05J@H_fl@bvYbCL-}NA;qUq>CHMLvsuH#DK@z6s{2kBXoY$N4TB6 zB6)u#EKKcrIoUrau6*x$yDk-@dgrrB4q)_%1PZz{bgbgpUXPNpLB77K&4dRd{9(RvM-c!bl!DK1qNU2R=G2 zd8au!87hF>6}O|l@$Hz`FHpq)&c`T-fgxwsyrkj ziwv@huz0rIAg+JB1+HRETxCcd9t5rc5~l0{rC`83v`WCY27DW=Xz(|GM&Vscc4N*K zNw+NCYEpr%G8hIXU&AhSLc5iQ9g@x4(*u@#k0g=9<5>$-`~Z5#%oQHyir(cOGJ_J~zwm)1EowLX9{2jA9dR0E>FV=oN_5tUg_;?Jm30-q$EVYu41YY9CS^;pYNUTH9 ze3L{>T^dMvXTzoc;+`v8e{2YHZ!{vrU06BWl90oVwJI3Y3j5_Tq(|kXl$C~mTrq6I z=x23;5INmAUO=$HUH}r2QB9$fHjjqBTA7)ig@& zSr`*15`S*Z@yZerK+P^LGtZaa-7h4=%g6K2jIl=dv~^%-LOwfZXNH&{35uOD-Xv3O zFN5Egqpz^suB4JIJM&^M7)Z>#^vaK z(Caf71qq1@BZ-UW2LgkONWt`!(Qq*eEbEu&>k9r;ym4WR%xXWG+c&H*#yCxXB31CC zB7X0FZA#b!jRPUTx`U#xWpTXPhz|HPYC0#R7@Qe-3+Bj-dV{hn3d7MOwsY0)xk|X= zCrtSI@|laL2lublPo0l(9)d*rtD7V*q^{CLq?-tu?*|(+oEh3V>A+DPxAe#^=KcaJ zx_*Gk-+yyb{5jAfBcypF>js z7&jf+I{=?;7o}?s9)V3{Kg1f5zCnFK)+6IV{H17Du=9X@x{~hARQ9>#EghLO7eU0D zt~(b9@7ME+c~q_+5V=h$hoR?gWsT^fY=^t)@SK0$I2B|k2c1c>x${ZG3MZ3NwNNnI zv&J2><9Bq7{pdf_G_42j`8GK5MWPUL8{S#=^xT!kHy8Df`iw*J45e&W{)ZQY{#Eits6ScA_na?W0DT&aylb#H&N5 z(r)wri&`B5kTV;jjcwPkc>rFBgilNy&7Kix>W6p`hojMNJY zKBDAQI4I5aG_)n2&w33UbKVPu4=&s8ybHqjXA!S4z(e(*n5C?vg3VC0y7^Um5|`In zUcxeUrb6|t#;EFkNxhJ&v@~%WavW!uG%M*56fMHTHa~a7iF3t^#skke{zgl z+u{rI+3+sqK2`0=QT-7cX7!WM%gnb$^ZOffXk3n4@1Mf{FEvq0sl31cQbC1{dVam( z3Jb9DJ*s7Bnw%Bvlk##HsRkrj^i_2CdaU)Ojs`B^1&pS~h`XGcASY+4P#gWQRZX%i z-mI+j4FO1)v&Ja~v*GG0i&Y|zr_E6%w50?_%ZjkHJKTtD$)iWd z-{JI|kfS#5yHu{0QAkUa2ZdIC3q%|toqfU9Y6rGo=WIL)Rb_r0H1oUbVLApScj+Z@A)v@HL#(-5AxDy$0vK1*SrQXKK z0*g%HFBG*3yPyh`bMHr->+&iW9GjXebJvI-2Hb()0HOGc)4v2_OUc!NaW|y(${pb| z`zg!khAuMUJn4&R?XGzQE$Wx;G`=UfL09bvPEX&itKDc`2>tG9F76q1zCw`(BX|#g za=>9j{x=MEmF**{K=!)SG2;D%{i{L(U`cZb$<_U5oAXr=)~>4fXU3gR*0YZIFO?99 zK(TI&bKBlj+<@ZeAl|mMus32;M!I8*c;hG-sl}F*G}EghA?-(vFHBLh0m|qD7GpSP z3PKXJ-2DoCWgB%|1Lmo5GXBhIxZMPm$g1aIwnj5oOlALal&G8=jRmKTYV4%DixM`~ zGOT1?A4jYUde#2++KSg3)7iA=!t&8fGK$m+4`taZj;5X_n#~*a2)Fct;3#6br#J0k zi2l^DdhqVLPBpX&BQn4E_a%HpPFD`1jhPD3R^|hWbjo&>=jh=LFh{ZHO)M>pO;UOp zR}^8_FUn0(7b)A{#gVfKm}%I}HfD5DA{KwzCD>bz&fHvr`=zl2hTAz#C-q#+J&gGo z=7+QI($Tb99TIO6=aq7Nsk6Gzs5?&fG+!2-O-CipX{m?dE9V-J7Vl6_h{E)drCY-f zHNOCIn?6{RadYlJ`uTMyCRqu&-weGT-!5Xfe=f92(FFfd<~}|uCRu|wVmopCb71<} zKk{?*V+E%4MPNnJQ?8*5$craws<|(4c6rl0U$%kRW>q3^h*r)_g9Mo^Wm^!aR_$)d zV6;cYjY99DdW%l|ZAt@J`)lUfhch;GJn8mIa(&aA>BNh5pzbGYe|hGr@47ua^9m{(=gvfRwDRttp=l z9zEpv4wN%7C&dk}dHj=xUP3J7v34;fFRu&k<91BzlR4T>8;aksP$42Jl{&RbslBX8pdC8K%gz%YmYueqkzGDOKvv5V#su{ zfnj?rB2YOm*ovA;5uJrpI<@IUzca_i2N=M)K{hCNuW*H;iomA!O;+E>C~B-mg;diIq7j#jB@)Jye2q^7Q2Fs@oE{Q*^MOP2EkIP zTcNqTT!$VGE_s8t&Q0op0uWWXf-?}lkG=e?m;m#kf=)g*#hUT38KZ-RKeHWKY3wrt z5BH(d+&5DD3=!`p;=kTXw+(YXRC3|stvw)3j?US%3oXmLG;a;gvq6V4xt{Jsam8CnGiDKG$&<`B*ZrNya4X9wm^!b;kV`oI-kWNH$50t6_v7c zv1_9@pQ}!!oX>8}m-CxA?&@@e#Fz!`H2sU0f0{ie-OrtzpEu8oI5w!+Bn^BUg=>nFY)Pce%)9x*>(GcgH2g!R~*vnf))JJ=b7rf*x# znIHm^|A05h1PYeOYjoM(Kx4|2g`RVEFbg3|5|GEd<(v&7=9C$SdDQAnx^kG;BHG2B zvl_L9L5&F^!x}Acw+2JZ)`GH=3nH8mIMt)@kqW)AvNb9KWmc>U*YePYzS$YwF!8MB zTOY4i3z?ewZf@>7A$Y_X2*1OAp?#hxeq_(<6kb@Sm~8Xe+uKl54^qFa7H3M7H$+IN z1MX`~J6213I79nicp}I$sM*Q~bKq9_{5OX)JNQ;#4)>;=w#ZgX<~-=$`@Mq{x!ooB z0pX`|M2k<9u&@~7;55vmDPTA@qMXg|&}p=wd=QWG-1^Nd%NvtD*Q5(ak&c_=8k6dj z-H(w?pRNtRzo~ht`0V^_4ty7|KmPmt{%dRztzhDL*MR43csjlD(GP2V3CnAV(Zjf| z4v_&4*ZAO3cc<~wxbCDAOBsf`6?-=j*J)gtMO4!v`!@sxl2u4D!C!I;vPbBmbouY< z(fkQ}uu!anTW_4zG7Df1%*eWyZxM0CG)!BhmIx+C3bn|r*#n+EYLn#Vvd$y`@(4<; zIolJ4Wfgq+P8KBAI{_B-gsS$@I{TTz^TO%1*30y1uhFn=c>FNv;FrvaM|~ba_PcVw zTvNCz5H4qGRo!nJBGIAsYx=r!8p;exmjSJCNTynY?;R3~5cY;pM(+*Mr6bi<8bAAqh{-Y+aOOmm;b zW^!_Ft0P**N+dh2IpxuN%2qU51n|nnA?6+BUk$p<8vtQ{UFbaXZIq^n#PE9W+qX5n zl_r@6=GHMMr>6!ka@&UWUox*ar+J{FA0hI$#j*u+Od~ju_`HKDPt`@(R%uKQ(|(w0 z#4QsI@+4IsZ`}w~f*7K)>GPqwO>l$Dw_Ohm<|ZtVt7e_<-OHPDmE(#tzGt8)()W13 zW)+o*yOx`|Mwo_>2M6}`@*;C=_}m57vomXL^#SpDCP&D~Zeej2C4UY&FYX%Y|Dyj>5Q!5t2ZS={!7@h;f509 z-hGaBiP+3JkHEs+q^y8OPLz52y}YW7pve;Y2o;tr{Dvc86&QdWh$we6vECk*I@NvE z`Z@P8x0YjD>i;ajE=0R7-fGz}#!S>xS>yzz{a4qJf1IVVxpJ44&Sn5PnnEbLRR@{!8?Sf^F zG2mw!VD;8ZkOEIXRf&HhKV%~aZzUl9=u{Wluh@t^(a$`{o%Xya35of0otSH=aTb3Y zYjdQ`r7M5JGxu1iSVurccwelxO2x~#r$P0udj4di7wJunjY%FwTAMi7Shi6u>fn|Z?B}7r zwD%{=MB_zB`pE;5%8#jmNVA!1% zefgc0*j$QnG+32!h(>s<6DDz+iA%Wvt-!G)=ef$4^JMC%fc8-N86Wgx-vw(Cja^{R z4uga9d(xl^Fdd|Gl(HJd=$-I-Hi&L7-3Q}I5(fe0D zk69Aq<$UoL1Ge_?pyb#R!ELxRCvcG=F6sm6xoGEA4; z(2VsIRGvjIfu_nMeAvv$N~rczqMdqoQq-?r^yrn4i>oV=(s2baHHmOUc2HpwR<=tbMQ+LnI&zH6yi8>w6#-O_DVkDpK&WqF2>ae1 z_<#A*KJ(De{)7Lq?l<7%>DA03v1`u>&SG^p-`Cw~4%%*9 zIQSZoe&=-i)%}3A*HO(-*)T~PQhcuVn>G<^#1M<@r|Ohe zu~9@q-JXFcOlq2f*Kbt(6F0TS`f=J6bwJ{Ftr~O|0a1ir&(G6)OAb#rweWc3Jiw>E zxeSvx{HH%|_t(s+;q-5*_svATbPXFeH|V>4gNFU48+TtAJKf%}`F#J>v$mQuj|FIr zag0*%645mR0g4Jp`HPW1^i^|F1J zN}No}IB zgup;HKYY{-&U!5DUVSY5#{!sz1IuU<*+F+Dm=r;9Y>l}s<74&_>oWf%;}~%Ym@`CG z-mElpGqy^sINBJ$I>zm!>93&yY!=vbzI8!}vl-UJwygEc0jFWu2ni9sQz(<+TFVM@ zv_HWAIQ}C7k@vDpSWXvF*LHL-vk>b=xTniPmqf~bR8kV?H7REp%SZsMHLbL(Bve=y z==}$BP7S*FN#lP(_~ZBBFdl~tz_?!L-}8ylhZX}~`%7dn1Gd90Q%z@N#Zd2yXLlzi z-qFRwRj^=Cp~S^%=B=xEX=qhfGXvtaPF^2KZcPxK2SjIy%taG-k(F->V>| z@749lM_QyNTbho3jWwXI^SI(y4o$lsfUfG%)1ms1sd#>|c(vt#uaOB(zN@zYwoHWA zwvC33jE2`{^t{l-t4R%7${Au{VXsleBs5;YWzCW=nyjW^*r;8^O8znfjl>(p6Yn5- zjFb@wUqN7It8(s)hc&f{BZ~342924DX&aIhE{6%xJk^Cp zub0L~0aS~lajhc!qZB}`fQ^Ez7ab^IJdBKu{bG3aum%_lhsfx32One_X$+;jb9Y5` zXrTITNoDt(*{dZp@x8u?*~OalXjP>Lv2$s4^eHyMmut#4aA_7@g;e{H*UHp^(P$R7 zuaH)+e={v+fl&%%2~+aII&>e^jERiKC5q3?NdQUr!7EEs29R#@n5-D@cy*tbsrV8d zn2|}wUOBGRR^#Ye41#O6!_R0C8OQnek+JX-{%*A>q+k05r5@639>sITb`PHu+HZb6 z1ux3^g(P^V&+)`pcRsVe%H%Y99w1#eZ$P^7w9`t_Gn}zk7cftvI34p;$0E4;xpIcci&?a?IsaQwh=T1Va@q!mr*3}eMfef#n2xZ!g&CBdm zAQCFfDfW<9f#*Olkbc!%WC}n-_^J3k?_^{;Aou`Ix8tj11X5ElBcIeb7^rY%qsLB9 zG7E7sd*n`r6o=s};NjB&K%6CdDow=}6AdaB5eE>h03g+m)yiu%Zoxqn@-vFQrN1$h z-}38Rqt-BUBz)2HY-9qvbl!CztM)-WRX#Eq4{1^-K(8F1Tl;7#=R3~$NJHgXgPOKN zF9fhsp?I0}APWsZjM4FWaRn4O7*g*am%K;5(*8#CilkIS+_vN(#qX=$FAKbTNlu$L%L61FJUPrfH^v%)r@^l^W{r}sr^ zG)`;-YZezR`57L_!u*uFk)y(G>Mv)OD}mPjWD&7m45S-033u{N@<>1eI{>&(p-*YNl9Eag7gWpwICyu& zgkUn^A?0eNeNezr_T-&tC|dnqOeV@)x0NdTx_|T|?yCq^K7EIQ1hnxz!f=cLios$I~ud{xmee7ffJ72KyQw&Ph7pe1Ioy4kyZ+zi#QTqQ%iPO7(m7Ml`y*2wO;^ ziHT1~8(VI~q#yV=hu7R#(L|F~UK+&f0vG+LvR`h^O$TX$fo#j_s;D<>Wi>ReavW+J zusc;Fgd--cQIdt(=q9b|6c$oeH9+F;a;!!rXk|5ep7gw!2t;vZR#&bUj1*v?+PTt% z9`u~X6AJNsdbtRu0t#NmA32}2S2%xTJr_t!Pd*pj)1JY>$vv^=9*He+5?y;=zj z1UySw5U4kw)Y8#=jw@a8M@lCqz8B|C}qN97rQ>7`aNbd%`k`SmA%u}7Dz^gs1>_@*N*uTLVK zqDvGT)7>8hUeMdmK@8rDIzbAIWAmGm{^Pc6{7$zPTomwJ%qdt4Jn@c9U{DpJjoi3v z!}WC*8)gKy`dx{os5F~C2+0c+zuPA>GwxIMd-Ob*xRMj0Q8P%HA5@Nut~*qYO$A&7 z$@GL2Q0MvD8a>~xs&Dwp_7me!Ft*hozli?2FAOva3Z|+Dc^33lo8iN(sJ-;wV$J?m z<14vHmP_CZ>DWw_8J3W5hr@UrQVa&Gd98L9USKl{SI7S7%v1;{sHeK;s7KF0GC)u< z1TrG75%a;?r-cJGp{fF$#yupetQ`T9cxptmi8qI-sBqE##!&$SVGUJ}k<5)WpS>~+ zFD_NxKBRr}HhrKP@SAL;$V!~A0**dknIIMB;^RL88@^h|THd4>rRGgVq^^q6(Q_on zN=bpbGRtdGbBgEHMH9|P991B5aUb+^saH5u76S)rBKjyWdya{cb55YC>_fgMT?mM+ zTOvM6+=`D!nhuPuZehHxjF%jQaYWW8?=$+DsEO%4#z?_`^RXIfrK1ZWh6}{&Oxs`w zLlELKdpLI;IHYQTa$Lnpcfab={hwa9e%)iSkhI<$bPsR<9eXe|wA@f*D*_x0&Xc7u z)pRwG0=_H9v+ z)k0#yS;T1mBN8~?x5S?4J(Ol4OwP&s>0sn@e3?O`q|F*e!P`rp<@2Z=DXT$|=`-?1 zIZtd7mY}s`qB~sJk*S&)H#Q-guSxR*jvzZ`guf-JCeATyZ4Idv0yX$2u6 z*xrlD#Ehn(D(*2P8!L=eRu^?e`oha~mkV9SWfjx|Qth=vkD7xb)WuUOm3&O-a#|fB zt;Fa8H^q&~17-TNQ%iB1?hFfmgqY4kBeHnx_c z7PH4t0=qa-G~h5DhaC07>gqe*aL)Yxef$2j*?=4~dwEhB28;mgC229(DbNJ>G&O`b z8WU5Zxr9P61B@xtMPnh(aaluY{Xu0}$b3q6dNiqYW!Du`?PK{J%8EuKGb}Z8(~btb zYDU**rKx;e!FBd|fU4Wtc$4h$$Y|75tH`d(bTpPm^aA=O%})3FIzV)_f*w=l(MUGB zPcB+rsadxW6ONHAKE@7@DqdFHyhd1t?pq8Hj4QU2^bSaXTGhM2Bt{TfoS=r67A|6* zLgl#QwNp*RRP{0+uNj>mmzdX@^P?B5bR28!D(6eTZ%qIvACGbAn(Ajby+g0JhC0ao zshRw1SSXo<;hA|jY{wyK+Xb65|D!+I^oRTBmUcF3rj*(Qlmu1|@X?|0OH2lJnDFR} z0eYCiBYT&4r6{Ra4T-xF@r$l*zOOtn8FGn>-s0mA-*o)Lbk-|Fh(S~pMx(1LEsomf z5h|JkQ)8Y_WxVOqifrPF=j6s_8@@~{DXrrAJ$#coq2ONnX!wfR!1t~f0wdlMCt>K3 z-*7VF8qLDPT9#H3B6;)bg#$Tf02+_vA?aH27-cd=xv0WE^g6_eRjN5s)kvW7x1MjE zxMg~etFTJ}e_#1-Wg1@Mq+%pEEfS^ZS<=r)^&jWue&FMFdY$9_!`CRXl+wj(<15b* z!*dK;@wnt%#&hKKnuAFP!hX!)EG`E~K>_r^>=F z_u#_E%rI04z)*RCER*h#nFz)Fs70z}dlhg21`@nE^!+k>QppF78;Mv#lfopbtqFnE z2@)r@+WsOs>*S*MY4V<&3c0~GtD2MJB&I_W_`FMVPj|?JU-R8sk0n)k)9)~A8M^Fr zEI3iQ=NZ_m6HWlnEASd+S ztO|tqHmCXur0xQxPnp6#tvzd|UPWg~5x@Z2&)QVX7E}2yH9wLTV7P>VuY5rjfGR)` z!NtqX4j=+!`rX9$8i?Wax*u`(<%>+(zsTe&Z&ZR28G=uGMEo8x(NYM$k>mV!eI+uS zsCSgnE64hkA(Kb1Ytp_vq!bT51nKxG*CGO6>0@kEYB?;X6P9##hKj8Cn65M-LEmJO$-ZtW4j>^aAfv4;s1AywcM$iEc42hHluTOYlNM=&|O;jjO3X zN}L8q>?W;3|J72R(Q1K%JWF&>GJW<{_bl%~^%Jc1=<~jUM4;M$k2kD(H~4uLTs7Y^ z?x9ZF2z;tD0iP?43I-2JV{y?mVD94McX-V>mPtx_QZyYPGP`Q@at!k?%6NW~o}o|a z@fz7DHTUE?QYi$Uoa$AA5-QmZNyCZ&9$m^l0g|3Oy1!(<SDd@o#M7-YiZhyK48-TaXC+qQqxWO;B_ z`y4y3BdNvoYLA69V?c(dSPP^(HJTu)UJ>0CImF~YE~o;jLA~L&v0lpy$?B=aizL)O z)g5tcV(AQ^RZ+H6Wdc@iV9_yB83msv54ehH1#9do`pvmS@5wMzr1H@mqVFY>jwia( zdER3Y9AmU^rCyEP^Ja}^QaRRW)}#c@ST0UYGu>s4zfrSKzh3~Db1Z#gJ90?8HSOl$ z;k{YeG9x3g{88xd@6WI@wF~ zu>*)eWrQu4iRnfcwwE#>uCkb^*5x6V!wA2B5)R{WSVRaS$Y3<%E7za&t=$I>jP@6o zXPYCW)D}FMbGeX?+|-CS>=`l?KO=5wvU6#L(KG^C@tvA6GTEI@e8=wvi4YbuD6Ni( z(LiPKi5rQonY7Xyb!*Ze3NAh!tALA_-!+nS4M&c)OyzI+T=Wo0xV%oWnA_qj6C|1@ zGU0^`Xgmyr%s9jCNF6FoKP zw&wU3N-Hqcwh)G5X`CqaQe!5DZLhsyeKdb6B4_0NAS)FlldAeT)p2xg$hN&-TVFYhN@Lo%gYIvWbadV*zADmZ#IP2#cniU|Wyb&-Qy9zK+EZQKe} z9~I1uh^`J|=QCHxXM8%2sO7n&+eLGGtx2wBM3kkZ&qUWY8+^{hdkz^m6t%%;;4mHr z4#R^emw7S%B6U zVJg9(z#=EJ)J4Ii?fNpYq$=RVpDKXM`NgL)=+FP&C5?4(7>~m;i3-65<&1|v`n0=W z@ygND7iRA{yW3s(YMwiTtu%6Loz1KSPkD&p*#$Egt?%JkH*Ca4MZNI`V6QbIB< zVlrN2IUv;7RJ1w|8jBHw^pt%_Y>6@=zSq6TEBYElb3x6Z_*{hv(0?W|5_Z4J3NP=W zgXdH+*uR zR-bp#Ea3MzNpRyyn!@utW4jJaTQ{s z#qaT~InZ}koHl%{Z1R|7S3q^8=LAZ2`K~Etfcqg6D!P?@)s$xKm^RKecsb#xJwqWL9&VCed2{pS*5u`@m23NB9drz-Ohpiq#} z-@Uw7&Jm@uR7+=;8X6&hrztExB|<>Wf?f7Yuk_v@mR5$2!+Pfr9v-yS5we`?>VuV; z7dPwcUR7^oCFaf56L5ADj{fIzR#!cRRNd0UE*QEs9NsJBOV^mN8WDK66mvB(22!d1|RNf z>C*n_D^c_^CAtZWQ)^x|sfX%`{Exx}9Id!mO*ZGXN~jRuQhmj!Ub1UwvE8DHxf8x2?!vrkV@JmxBU z0!W}J?$T&V$$ORM&@V+2psN58lLw)~6!g4GTv8Npv44>;faG0q!nn$GUerq^LN>ER z^#+M%1eNQj!EEy0bc|vGi^7>BI<-%s4T5nPG9_D>DfJNA;rV0qfD4WR@z+;d_7qDPH5aAn18e6N=Bby7Dpk z)}$ke3>xn-mlnQ6Mml?KwyL`ab_6EGZD#f`iqViqc9h0q5pv^lL>B|m3fyvbwe zsWeX0O)#%#YF#ytKo|$Vk0*y0`Y#DmLe<6Gs?@RJ=9DHLZ)7=@36QiC5HFTb`=n-v z0+UZkj0yJ2Se}BxMobeqr?jfaU`AnBw0+CsJmrLm>1E`LUD9;e#p~CYIF=ECfhu}+ zWV_y_>lrmGIXBui#W89cIXJ#H@_nvSoJyG|_!#39V{g=JBy3H$t7#2PdriE@piu*u zJ+S{%Z+OG;hjEdHG>#|}>Ar@p03KF8lp9D9onndzwWqhx4OS8E#ANc6fL4mh!zKaW z$fbnDi&x4gPe@wn#6uxMrg~lgl7LDuK{vJ|EyGk_S8qjGrW+M}GGkB+h~6*&O;w0u zb#;{l`EK%>q*4cQi0k6WQO!5fKV#e{NrA$Vl=A9(_ zVTXv-Re#ULh4!sGql_FIZVw zC5{C|VALsg96$KSBIGr*kpG+?&te0YqkiFC)n?JDsy>I@@v+uz* zX9pbqPjd8#``1(SaF|6?}vI#v_Nj@L<_BcKgjQi zwaLcggsNl;3L_Avpu#IaVmrzPOJ!L_JSLiC)z&g}&I{i3{4Bt6#{*1HOJ|6JU&r^h z+5n4-U_2KOi~2~s9xfeIQKbEs2<_xbu6!>w_6t9Q0d1FD)8&Bvrrt(e5z*6q!Br9y zo21C_0hMhH%*lg@zqC&neTyYq({~ybc`0v=L5r6OBmYPuR}nl%?oVF3@t3g{Qq^H` zIFBP<1_i{QSjNUR=TvGL8xc zqr(-Q{};SRnNLC=%~!G8&6*oC)gs}QvBGRz<>n;DaZcr8q^SFbBnlaIl-e&25?H>p6nAeom_U<(wQSq~1XkD+OS7rz9AHV*K7;lCeNN90 zT`yuQrHNE5>K%!_q|khH50_CZ;ZBTlJ+vr%DUZlva35n4d15Vw_J5#xZMq%KDKf4| zY`3fg-SItSbc4mA%;5!4l1iAq}7wYh9?(o$HkqEOo??G1s*l0m?1GU(AAu%6oXbYO(32F zub`yoB7%{aV4yK8LSdrmS@4euuDmnOU_W=Q1`1w&&CbL2&i`l)qgsEvO zF^l3!f&||2ddRUl^$@BaqQFxm5E7x6K3|j)&Fdki0Z`mi1WQBb2hV$K6{|o3v@LdA z$-bypD_%4!gt(Fe7jzfb=`7rH>8obWzw9z!d+AHf5yM_^97QG)8xF7d!qtJeQ%~l}TYHWv7_o+)KS*fLKo?iofcD^pzw+h$c|9Z}e{kAeqjoVjp+vvX^rsl_XjNQb^@~@k5Vv?<`%W&kdBvE572~9HG}vg>Ac44f#w~ale~r=mLN&F zjvr(r%`?up(qKmoIKswJ;zhzCwUElCcQorGzt_hVStztIi2>lvA`1;qj#7Ze$V36r zHzY{SNWh(wI1kiyWUROf)Y!ePWh$Iv#%L z(Z+PU;2RBo9CQnaKB%hON-A}1=q ztD9P@911f;_qdm1U&Gu~O5=+r=K-^WOf1eW=gY*)Q33I|Hb}4}_AEiypS9lCa#LD2 zuwT9Yj@FrHo?$P(_@W~YTyPvk3+6>*KQ8ed^O2B2EyJ`Qn9qsp9lHeD#A(L%E4l;XJbnd+*E z!^vmjkoS56(cLX>=eY7lb+)=o6?-JUx5A9><85qy;U6W$UeWk^(LY!t zH_+>VZ?#C&rO)8R@AVjf0;_>O0!)YW7X%c4`<*=)AFuQI#@<*1(NEN+?b)@gBCeBQ zHz&{2X{(65`1!tGKe;Z!^^67LxqE1V)e-6WN-rfW{o%cY3V70F=)800V*-Dy@tT;g zVN*LWFIS9>1xB{K@XbH`)8+_+3mlQmMaNNOBJsf1v|swv{4?9*(|bx6T!o%c2Pz3e{7Aua%p8m?&PEuFKH@CooS9NW)hj6jRiRe^Knq(L4eqj4ouria%KN+s z$n>=2Nl8WPr~yRtg}1|D`HqVHh|vdoX^5V~`L$JO>(h$ubKk)G#v=qm&s8wYK39 z07?hrmN}5Fr?q5Nk$%S@;~~#YG1{{E9e3|}BY}`-ZizrTn2E2kE6;&;=!c$ z4_~(TL+fYOKeammP|ZRt2WG#LXqTzT>qS_!6;aPfBBGj709r4=NS+XQjc=iif@-)i zf>tJWtO}>vg#Zu5>O?(7DYxt&>jTl&*T~SdOrA|A57mg=WX-6l#Q(Ao9FzGt&ZOdH z^AO$FbT}!ONG-m2{zT)z@&hotKLE=a9Hon5Et*9VN8^K_Yl7ZFjD^4@0LtPL1=hjr zocA+R#(e1NU9f%YIv5|#VS2(ss~N6EJqe~&SEzz0kt@!Y_ToxrH~ByqQ%SX&Ri6pR zA?>dzdot!RQVu1f1=o)t03lo0g%>guW9SXJn9iAHGaJuEp5q&RF*<7U)urz8(#o!L z|Kg1&+=$?E`Q_&x@!*2vC`sFT=QE>72 zMxJmKVdpVoPr(KUPt=Os=~>r{B4`Z65>(K)TBv9kj%{{hlh_<>S;Jmq&-N)QZX0h9xO%-G;=s`lS5i%BZOQY8a0cL)|dz`*E+`2!QHVr@4ZkUO&93}$5G=u(9 zuhYI`Dg>7!Rpj*P_cd`bY({as3$_^!C$OM?!U#CTS+EUG)X^kbK{OOr*RjbT0S-3! z3&U!1tbl)>DTCpx=+S0iWrM=Z&h7i7SHE$~pJ;y3;hrJ=-Hf9WV0d`*k{8|atmBT| z{_f?jls*8=HWWoy{RPa7wJbvv6*;|LJPI=TWKyT7TsD!obqEf9PFMw3Do@W>9X}=s zgjzl#3XAk8u`{-+qE$^BzOr0c%1h}2`7U*nrL0AJV)Lp3as;NCIxdVJOnE_D(y_)=-9}# zu~OUE1;A%P#i&O}VhS>e{0{r$jOBAZ)&|>I84Q+I_P?b$*ZYGV?>@eJ#T7oo3XE`+ zjiVM|q}P&r`O6M`eP(9*A>CfMZ^B)L3!S)i$vuqqpYzNRL!dC&aL1{a09XIaN6mvB zF-FV_d5eVY$Iy6VVo)7(a>g@q3ERYgyrI?8u%2U6OpK%g(065A(8u;Kz2@mg!s zMCX$Fmvw(NC{3IoAONJX9x=;0>OB?nCF^$RoFs>8^L4M1akBbXj2(i0of zV~1qkA+TuhoPvx*T)L$OW40nABDYpTpJ9I8ui3PA;v{$xoBgMzbQy95~`1N;13`@F|n|i1VjSyG>?iVsuazlkp6*0 z-$sD7YWFc{jE;CX4MXw!{6hhCJz_8hUDD_+!p{U2V>WLv z@PUur>5thw9l#)PN?Ri&mDC50GZN2JE|XoTK@5V6F)c0$yfn*WJqt!twu1@JgXv}s zhVJ9D1YlC5GiN)L*8?%j=n7-@Xi=k;8+q-emtH`dzVgbW zkF`|CQEVddKqm9j=ifBF`M8tz6{Q)JrKzba*U^or>I$1p##Boj1fa30^F3%0HLoim zBpXA+_Kp0h9Tq`Ml*+Zx#7kWzNjwxFiE%dwAQ@XFCi*v$CgW2gByU$t9}U)1_cDm- z=fr#>5E5$#MalyQ0A@B&u8?n!Q5*+r87z*21i3+blz%WN4(BE26cMX4BL!@+)akAMYcc45F3)Vg7!>tJ-3F*L zb!_DqKyTS(^)L39=XSmEFHhKW&>dxtRbA+=hXv7!J-l}_|V z5WpCaYHsZIba7&lxsh8)73dU@)O+O8ck1`ZB#cG3X@->$-SO&e!cQES^YFN^5VzYT z!X`TASY)lrJ^1;uPwr>h6ABzg-D4opxdS$f!jDnr5V(gwAcZumQB7x0y9_k?p> z3uYDew6bKP*eqkARc*}@xYeteY!Z#FGEi#ADvZaeCrRu+D-j76B1I$S(jI|qX=&*r zW9!DA_L^6Yx$g}uhTu{iM?JvM4Y9+U*Ma%^i=KPalTSYR=|`Yt8X46on_= z-*BfF)R>FnVM!7+Qe>9?!q5q$vs_BKdM?{bVHVDK$q-bkujHj-$$$$J3!N667+11K z38Q+0$wYyBnXr*kL-|?T!Q^7rT4e8Hyl0quHVSLM=%R(;dFSJ)4mgy{5hMqg%;Afs;&HQ{|Bu!>J znFy*>usC^`xIIfM&0tg;K~pHO7K(8})`Fmd0Hc9)0bmUM-t%UB)WU6d?S-3e>%zwM zqZkShjxFGTa(|AaDplaW8Z7#H`-XR(aLt{!+Qy!9gBdIAxQzzupzC%C6kxLsKWzZ8D3B<7;Txdb<~ zM{##%(!fo(-wQY1y6QKsYm@vOP>UE0=^}=M);Kwn*&nA(m;hoX*1vV4+X{1G`9)Y9 zrDAv&ubbs&Fa*K@IL+t?8GAT0&k&GDkbJGdKtx=O&K$4mm!%uibEv6p!7t0Xlbw1|Ys&Ac4HwwO$ zctIkBDuZcZEDsVR;3e_Ua+G%XDH^pR-|=TPqdm`IIPSMGWH1K}G?#c4yV}0;wNoeV z*}41G^_t7;xhcZD_SQ;WZiLvNlQ&JsOMhi8zk*H|iU9a+6w_fMZJgZ@T==_a-{l-Z+>x^T3ciCe>C5I&wNJklPLLddkb=Xr>`9W<3 zJwVVAPf$1fz7kl<@)DQB4D7`qi$Q?t^gZcA>!%%D@vq+qAk_3z6Ky>8o-4drjNtW| z5$_Js7`8SkR-cN58>x}Y?mj&Jl5>lQF?x>Qai!Qwgg0VhGEh4L>j?&z#yFgj==GRd|WJSTsMV#wwXe zl5GGnh)v_ww2Yp(i>cNM#`1ZoKqK!ZelE^%3K9;YFgj64ISyAVLogx1ILs5QErOWF zE4kHp91?qNY;~l;XT)H#nuWXW__*o#FZpA)o?)8xMsNX-q26JR+kC3;thPvi==8v% zE0~(@vWwhE?o=w%sjljRV_f=uPFSo)>2@W~1x>Zpo`cOBt-1BCeemI{clzzyHgIHj;Mf)vc*z|d&e0$+F9fTxwu6s+`kP_;)xEH2c9#~0D+UrUZA2TDLVoIUE`ip0B^KDYg#gu;y*EL?ZfF8Ibb zcfs~!Hc)aFo}8S7By<%cXk><~?Gl=?2rI54iC<*TTP8JUEGbxqL@y&|6JIbJTR_rr zOKOKPF@dxmbeHxh)b826=c(^{*Y=(~TI=USG@3`WAH~||r*Wk8W zKjA-rO=;R=+u(#_XDG~upv16BO_rAt`%Z*uLo%3Qxf{PMGch`vbHX@#(+kjW#G-}; z0BHQOH{6_>(hMjwlrXDoG)#VA|Ng&x+uM%$w+$PHrqKgAh9=VYF;rL4NQ@W1`1Z+s zke}*mjWcs+pWsZsnH`~)B3F9-LD3!fo9a;f%icx%zCC;A#|PYC*=HD77Hj~V6DZF1 zf)BPI>kxd zLwK_+C*Lo+%lFOR;(z>!e;X!y!vxDIcy7dtl~qbXI5vkISD^FQxU3tVpjIE0tz5wP zXwBDZ3+C_s;aC0s{WqAIb?5o>&wIOBT;7X&?vbtdz)~>22h)->WG{%eCgYNm#5&Lv zG?ZOPG1K_EWnBsxhg3{pvyDKOBL)q7vT4T&v%)5G?mG^7?tQCe{Nm!mH(v9G$#aHY zqX&Kr0p@-ju^jY&GoJZ#H=Ta>-sbfq?V4mT8;#H)Hc}$O1be0scgW5Ivu?cAg2$Y* z^^TFz(G!AjT3AZtW}O*_k>UwtTR3kHt~UcB?KA^uTzBJJ-QD{?WyV{lhqtbBPbgnN zZ?FuTrXK_6JoGufUmC;O2sMZB`Y;?1=op$v z_v84V+VYF5=4E4}V`xZ8%Sf#{gK=<}A%Je{0Tr_(GHg?*t{K~FZ z9lveU?=E-D0Ow9orrIqM0cV61apW-PJdp~7@gco_-<);&Zy0;ydVj-h?O zYz*^z6uxlHoBcTtyV$HOG<+?H6;?_V_!I|BRq0Ti<^Vp-Eq4_BcftuHaM@o!CAi+_ z;pCH_8d%Oscux;aBEPaa3!GBK)FX;U;^!w;VmUpNY$nh$HOO*+Bds%zxYkm8V5CAr zto%*j;^}KL&#^*i-eumkn#L?HF5UK;H%y##)m6bec;OIS9*i-JgLzN~S2XMO8^4@q z&e!wcnyuALqm~CjHG}ZGCDu?2b16sf3Qq0}%JA>ZbmP@nPxxK$+Wi0Ba?1^muGwC# z5%lFeEZiu-xTzu96dyV#IX58W{}jpA;+Il@lkbzp5c=dW6U9!P7ENpB(SjO)Z&5Jqx06pHm)6ZURqrP$;X>1r5f*G23(Zx;H;G zc!(YgMQ0@dK<*q>bnKi+ z%md|v(xl4hk2kp?c^i8TappCZiomuezFzl5tKsuockTK8H@#`*NeC{NUw-5l+CG%V z5MUm}fo4FjGji!@yY6av9qP5ZX*TM`$BBjbRxAi^5kzu|rVa_WAZ zU+B%kpDkOK{9E3>{^UFEy7PDI&7wBaA~7~>*N{$;*E}W+k^oAZ?&3b?Q$H&-hTOaLu-9#uM#8zwDA>4xJ*&loly zGkDB_t=Sqe8i{quIKvt96Og@G6oK(DNC2ilS-<=hwj*pUBkxvt4a;XDV+o+rsH1z` zmlJI;WxwCKWBsTTqFhw!&J*<8e6x41XBTi?Z^KTzI?L7yV58#S?7Z$(%i{F^aC$@fY{f6ns_%cW& zMesyXevro)9fR&8DkF)h2A&f#0}U~y6sEx%p{@EDeB;L7@as2j4K8wkh4~K5FD{zh z`&NCY|C?~k=Ds;`+h!PRk5W+F{L(yJf8#gduDf=_=_fC|@SZzA{BXE)9t#oMB!|2PXQAk!n)xH9{{N zYciJYsaSw3f=ysU*pw~UgZ9Au{2Siz#&y4qB`dGE0$I{9*5wB=h9d8SI5tf0`e9dY z3@h`%I9mg1C5r#zq{o1bB9lyKO8X0bodNNVb(0(3uO$|$1HlDZ&Bjw;A+VaqKL1y4 zKBF}<@xH0)b!W92)~&3v9iiO=UsxL0m0+JCf66WALpYLBtoVa)H;qIs0_(xX z#J=I$$Sh+gRO~H|mFSAHvU#!~`F4Js;fs-uJFs{LU+|!PcYbT``-F zJL!fuymiCbd+xdZ*n4-~`*(xE%3!pOwQ@}`zf3S6j0=yPP#9)+U{I6;lu=_aFk99? zB$zpEKPagVO8}5DGd+D;P(=R8w(`xeetH}>&P>3jnf0)7-AwpQ27|JLJ+vouZ!Ws% ziflbhJa=hnr){;`F4$?NJz~r$r~gp%?eBW>!*<^F#s8Dp-t4-GQJ9!&L912wtyT^} zuy(a3Kr;YRUdu3R+5`)dVsYvXDU(@`KPA4m74+k=5!9x8OZyiO{MkxB8-3%Orhk0@ z{)t6AA6H*J6mx&jW5{40gdx$tW_EA)ae0{FsFNrmq2%C_O$JwDp{1~vV>wQnQ^B>7 z*8E%kC*L6eL{%9!90&IX(0OL~`QtH`03e0a*02&*nQzzq zxbn)2F@|~Rl#_n)pR-#2QyG<9$YFAFrrfZ8GVwX%fL!fMm5H@P{g#T-7K7e_#iIAoqUgWrt#94*Mf{BR9l}D} zL)LO=jNxT?5C-1(t8@FG)@aRq0Mm%DO1-w?p`0FSuMZ5MUmp@dqzm{&cI| zehfvuOJ_sSoSxv@KruG}3v_!9y4^we+XdZY>5dQo%a#-2K_1WeiGR3!*?;WU)=h5r zt-xaD7JBCPJ3bBDw*0GKSh}7H;Ce2H7c`k}0F9MvKKt1xKeDn33TAoOBcAn{V~;uR z{Lyxel#->@zPt0*TfX>>Z@ll(uqMd(dl|#({fEOCYA_G*pn5Q2m2I?Ik3qc!^@N}| zP_5fqgX1-o71TCi?bQo+SP(Rqz_Q+yJTkU7f&hE7(Rh_fY2ybh(o993J(K|K-*7AMcdqZ$J z9L5k}9_SIK!JoZ+;R&Oo)`od$1Gr$VTaN)}*eV0XRV*XGT7z|37oi*MrU~o+AH*h+ z58fzY6(&cY4y(O=!DU_q9i{>6rcd)ro%g}`*p>h%4UYhm1#qbc#0(ls?ci+$4ak4y zGjG4Gv(mkm;%8obgDdj{3eJ5r;1 zuoxgFfpC;}gZ1pF*(?6uH05o7`M!gjdObL!SIoH+w_XJM_g^10mYk9uM@Fm}A0LPP z2R;t<+DLFeGq}|2Etl8YWB0Kcl4)Cc`-QXn_faEU+)lHBlx^^g3oh8vMC}EJFi`~EXz_A$5WzhfIxA~z4`2de0z&wBhUDJQEWB%NBJIBOCs{oUw05K!Q z?xd9~X7>bd(!jxFqxNq91DXKA;u#Er`h4>7&-OcaUtW^F?hnUO1TUEDKWz4$?HyO|__n|{1d#CK9`(rQbk2C_nRTo;H7E;Mm|ud0h2>}e z`&DoH+wbNWx#kYA|0$TC&wJiAwS`kpJfm#Zf5J7)&jxCBs>y8=17=*0OIVarZ4b*# zP~4BjBIKU3!x!cYu!Gf=V3K}#`!@T>;WFK|I=^H`nytZ=J2ouKmrDNVP+0zM#}Kg} zprPy|%WF>$rjWxZ-JA;_d2MB_J*l-wIMvs+xv6J^_wiPKFgJ89Z*(x|t>1TW=1IQ> zci;7uFhM;<{x+JJ0JP{=R&JxHw>-~XZtHgEDVy(eu|jac$1H2UYIUV!v8Yrqll<8D znCo_z-^h{3{|$@r?1{r)QPcX>zg(R9**DDo%0++G{q%vyp0n(?O@1w(9(hGxYdthC zvxeyz-^Z4>D*#J#0NDO^Zq~u=w|3y#ukM8#zrG6=4$Q$wV-Zd{#kq$(vei26F;hRA zZ=d|eVDr>GoPNyg;+E;93*OSZ`H8Pu*zxpBcVRq^-*J^62BvRmKmMj46u|uUuiy3; zBjd-v47Hc>(aitHmtJbtt=s4($6Hvf-!Ck6%pG^`gR>s`O24?YWCmRe?e^8ldk@^KmTNv9YmHQ$N$tf`Nn8} z-y@4=>wlN+##8cEKAvNZiWTe^#dHR%Y+)%1>@taUgZmv9MCuau?OqA)^a3_aJJ@vW zh(G4|x)~j>`Ci{pBRISkh#K52l1r&$mjwJBpF>KvxZ=RiF8<@^E}449MOXOj%8S4a z+06fJ3=#PO8o1KF<#nsi4&dUcDu2eMbiLOu1H@Xir!&^^#_VQ4C|4KW{`M1Y{6V;< zb>w-roMjxBzr5ni!yobz|Lm&$HwH z%zC~)uZ8n8SPef*Jn^x0FSSs?=5hfPX6GF2zIVlVH-L5PbJ)5ygGWBP4eQqBRAORj zk-tuY$Bf^RkppmkcSP}{gV2`)_hxyY#xD_%8xAW9Z0PE#8 z!B0?V7HC4dox%2F9}BnLc{!YV;_tY?!fa=`KNUW@^E+)51=O#8_0RtI zo;^D++FC!x2M`OJH?Fp{^Nc4y`Q^v7&iT2?Z2X=J2J3piTyNUb1~waS)dDN&1@P@* zt68SogA#1N$IN4O$-&OuedsI?g7^$@%&`U@c2*6h){jy#s)a@F@4dJ{9y@5}D%~)! zYme@VaO6BlmlFh@Qp^5AJ_z4AIoixtPTaWvF)zOTv>j9Lxp@Z+$A3450P_G0r1)?C z^W~Y=NOqjzUScsYUiv%Ijf?35B{ik@R1+o8dz1Us*Z)t8hd)pwz{)Oe@!Uv_Pozt)H6<=PPd**uMQ7 zbN!9~7q)NxU(j34>i^w#u|U=}|Ne=ezWdI*@0ohu+hL;J@NNlgAS%R42IApCNI?+8 z-#g3L2_R6TyGy^Yuxd^_-NR4+%m_@a%gyY3U?0eC!s~-BP}Mt3zW_da-25K+ycgU) zidyL6`6us^VUTxXwo3gizBb&&PExqJC!Qx@GFh|PDEChO(6_#_WBLSii|;T)Q|UX6 zp%>`^8a8XZ&`Gm(!_60CS$@YWC%dafOvD(Nyw?2!zG24?*tQRIYK00vWzu*pfX(i` z-k<--=fTZ4e=Z251=PcAo>{jY?%n-&&>qEJ*dw+7YMWYpul8dved72F-v)Le2e$xb ztOceX%&=Y%Tx-~SDbr@&K~vcee(zonU;W~oKkd}6dBF=t;SuMIhZC8@;(XAy!aicD z(7HB=v&>RTX|2+Cl1v)d2(m+Z3!1}L^T7E9wlZm}(ofE2rPubKm0gw@t&X`th z@a7++xU0h%H;`^B)DIhlP)u()`P7Gn7i)n9)L;+}pkDWI;_>JBuYKb^Fg3RAI|2;G zsvx-h#H;Up*}}x+yJ4U2<;@JHgKjcD0%q6VFkx$yGHC+HnAN~ab_O-)?mG*!cjrEM z>B}a}_T$?ycYsCRHW!~!0AMQ?QSUFc1^=!#*=!*?1(fO!B$tF>@pa}*0$vvnl83ndvj zryUUK5sOVx59fZr1Hl0KxIYBeLqrO%B`W0+3vch;VUgXKG4`S#=H4vE4^&aWDh9U!(6p~pZM z&VJ~5&|B$*30eyixHPr8_gl9d7fh$W!n!qU*cUQ66hVLd&5PGIH%|VqY_UJ6wXhT~ zz^3WIQ1)OQ9Coo`A}Cg~+ke3O!N8mSyAQx~p1aZ9bw^+?7k+zA(m1Ipv3-AAh};8E=hnRz9I~hR|4@O0XLcS%wVYbHzm6pF?|bP_F9vF z#=(NaG=uvZ!;|s=44-9BD8&TQrij$Jp1nr2vWj^W7!0;^^mb|t-Uo-uu;4?14h+7ZH5XI-$eUO1ZB34y8N5Pn zb=B7pKQV10uY1vH8VbA0NC$rr&e^_!GS86uECTf+mBW zp}<;lsm+`7KG5gX^HaoBi=>+f0FevYNPwf5JnSK2;OCG56PZ_4dy$|SW-Q#?Kw_G> zl?t5?H}FM&bmn!;iH0oZ;4wTQ_v4_x%f9x^tY*}_pp~y=w=~)RURVsV7mW2Noa=R0 zi??2W%r!r#?CFEY%F3R?yI^EEnrCbD1#D1sV84g6AMz}?ch8+cI|^D%-NMZJP37X; z?ziGZyZrg*4xajoogbQ<8a)YnG-1<(?rQ0^{RnC)2WCr->vs^(RcF=1?%S8lwv*aE z;1}r~gwEoD%Pm`+hCOk#JJeH&%^+opURjFlxlisO0SC{l#5jp{aWOZH=}9uFK%Le4 zT-6(h|4^^EvGM62e%Wo0tu*i+4jDs$xt~X#XTOMGVr5kZxjCIAHj)RUAbr3%D$H+# zd$#{YX$pTx+@M>wJfaNyH;}+I3*tSo;!Wun1|I*Ir^Bwh?*Q&ak8M$GHa<48@kgKb z%Cmp^#b0@XJ$dtwWdrYmX=Bktw6su|k=4sk??DZh!N47 z{t}Qg(hJ;Vg>kF3_B#ZI4v9tLP#HsjxgW=y-gIERR@;^i{0*hca!V4QE&K- z+fMNNZvD!R)ki=41EW1Cq?a1$EGr#@o+v^pbR91SfC&OOb+kme3I$rM?9v*^gPCOU zJuaA3DE!hn!_N$a4Qk731p^!|@d`)rDy4vmm`>ngOpCscJx1I+MnZ;DI)``CJi%)q zFURLZ!^$LCIG~jE<1=SG=EXOjaM4A>IaM9QlW{){3hytQKisTaa_xFKw@&l{Vl4QC zdN;_^;bFDghgxmv(}!ByjedV+)$1H3`$2RJiE!=qJUs5v7nwWn_!b4PVc+O%u;|7f z^Rquy8^DOAHq%k#pl%8HVaFNI0W{E$j9}~NT#%`hVSZ%^7zfi2@I>hWCH)?BDgtIx zvROoEVC5&CI1;(g*TcYJl7~n=MjZNIHTryfPkgIFh(r||DW_osSd5W?JN%R5oD86P zz1*672lixw;fP}hF!$3yfGAD&8{x)tjE2|(arq_@u&=RE`J9cP^it&Z)WAl`=Kc699I z*$EpQ6r~}r54-FG3ktv!Pz9gE0#Uv2xLVMoN02E+JK++e@>98kBZ(1)<28yJDw+xi z9RYwf*d*o{R6(fL8tmnL@WNn7=qGtAhZch=2mPa|D=kO_KAztbZyK}LgY1%vMbJ@JFSbJ!~i zpTmApok0+QIpiNfhKHwE7_WJx#;+A$y@Ud&_N8W(XU3wSqpFFbyRRgrt^pZxd5dWz zf-pz;YK)M@Dx$fYw%A?W!Mht zEsn(QykT8|S%<+3g{3)H2ZquBCN@vO!mc@DO;~!NCngd;_W8$4Nd>|XOpcUv4ApC6 z(43xB&(f@dJokm+f?r-hM6IGI&qM|z!FoWr8$MOi^@LbY#&d{}`q3NBmam(sKlni> ztR5<32r&242rKKa0$mZG`Xb7LvFq=Mvb`GL|S~4E3kLxT>Zi z+1ZRYPM#9;m8*SK0__xK#K_UQ$r^7H0L?a<03%HwxR!%fgWrcTj72-G@5(`I`X#u$ z3|Y(&VD6WJH&QLDJq$RMoeLIursY_;$g_4p!F3wi9wvxwHOf!GAv?So^p;-ljk?sS z)~&LVn6QaVW$zyEPe1z|W_hV(PYtQ6`Mw95H{;_gXEy3s0Wz+5R6|6H8Ij^wGRUBbgy{i+4Fs_!@BhQ0<~`C8 zCWm?B+fMw<+}zS~ZvYPKso;duk_ctxv0E6^E?aK7PjM{)R6NDT^u#5PF6#Dp%`7jh z`l2^5y)Lf!#*D8Ug>GOl!S8F3t#rDMGPzsp*O~pd?hC-<38W|%#<9z~C!kh}zGJ1G zw-vvQ#Hy$?!sphc*Tjp(DFLFrcr8FBLU6N%u{+-M!~X2Krw=#Y(1mNi(Se(8>iWIA zd$78Uigihax6m5id3T3W87w74T2!N5e{{oN4ATdO0P}qhG;f^Ce!kvJ4hLDcyey$C z^lEB=<5j)a>jj}=`Ll3{4%HC4odbUs%n+-l5s9<|iQ_4_*AULkc~_V~GJG#gyfWx1 zWr6usbgbkpYmBsfccpKNp7YpPE_juQdAQ(tDFPDpyWuBn2Pf*f)7HEBdk>ge(6owy zHx*Nf0`B^i{38T}I_4-zlraqzV<0guE)EWw3+6Rk0RP5zGL&=~o}8U8;HDb~{u^KI z`de=4`MCq79|W=6mfmMUnD=47UEi6YNqJLi!17$j7siNA!@>>bX4NJqCm;5ro1O$m zdHTQ*VD67$&ClRXoUtdvIQgL(BkVQu;*CLp7rO_9$<*C{`Q@kXKh)gQ)q&j_-*)-- zSI!^k_5)D3ftV=+F&_|I3daCqaYY~Cv4%NVdU8Eu>b*awzWueA}@9>B2XDF{rL3dM|=9fuqN;KHBiWdT5zwPrtuNB5^+xE z5qf7!E+)+dkPvmOAFKC24~O``-qDv{y2CE_e*Tx%wNJe=Fbro%u9d3%fB@r2ZyH$* zpc1%TIlvfRoL?SM2h*pSMFdo61u>(ywBobcxakLx6q6(;H%+*^ckZ)O0cZ!^(qPB8 zv4)4Sb>Bqkib5i__4VLQR= z1$%duu(05vLkXBRU>{$kdbonFkL<{?dnzYdcY04aT)=4lq zQ-?Z&?Lgytfr4W&!KsE1lT+KzdHz?={>ja7?a`{o3<2i*7>FpZe#873_4))*;Q~oL zHfqx|4Yvi;bdG-^?+U)Ryb=zBamgi@;EMbY&-q{Xd}C%}>q9WapZ&a+VpuSmFpv(I z2or5&Inb9JEkY}2!b*-1YEBMD*G<5#J9e4+SPKTdEHDW~ssi%hed;ZDO>HbZ4lIl| zEzE2k^>^NQo7pgyq34M!)dhxxwLAxbfs`n9WL$Vki(+YkEY)C;N