From 6514ccf9bc5513aaa1a80ec9c9d4e784fa33685e Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 21 Jun 2025 23:03:26 +0900 Subject: [PATCH 01/52] =?UTF-8?q?[#234]=ED=95=98=EB=8B=A8=20=ED=83=AD?= =?UTF-8?q?=EB=B0=94=201=EC=B0=A8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/LoginViewController.swift | 6 +- .../ViewController/HomeViewController.swift | 64 +++++++++++-------- .../RootTabBarViewController.swift | 52 +++++++++++++++ 3 files changed, 93 insertions(+), 29 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/RootTabBarViewController.swift diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index 2d87980b..f360e805 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift @@ -95,14 +95,12 @@ final class LoginViewController: BaseViewController { } private func changeIntoHomeViewController() { - let homeVC = HomeViewController() + let rootTabBar = RootTabBarViewController() guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) else { return } - keyWindow.replaceRootViewController( - UINavigationController(rootViewController: homeVC) - ) + keyWindow.replaceRootViewController(rootTabBar) } /// 닉네임 설정이 필요한지 확인 후, 필요하면 닉네임 설정 화면으로, 아니면 홈 화면으로 이동한다. diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index a5340dd8..b4d81a72 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -23,6 +23,12 @@ final class HomeViewController: BaseViewController { #endif } } + + private let logoImageView: UIImageView = { + let imageView = UIImageView(image: EATSSUDesignAsset.Images.mainLogoSmall.image) + imageView.contentMode = .scaleAspectFit + return imageView + }() private let tabmanController = CustomTimeTabController() private let homeCalendarView = HomeCalendarView() @@ -31,11 +37,12 @@ final class HomeViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() + navigationController?.navigationBar.isHidden = true setupDelegates() configureUI() setLayout() registerTabman() - setupNavigationBar() +// setupNavigationBar() } override func viewDidAppear(_ animated: Bool) { @@ -46,12 +53,19 @@ final class HomeViewController: BaseViewController { // MARK: - UI Configuration override func configureUI() { + view.addSubview(logoImageView) view.addSubview(homeCalendarView) } override func setLayout() { + logoImageView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.centerX.equalToSuperview() + make.height.equalTo(28) + } + homeCalendarView.snp.makeConstraints { make in - make.top.equalTo(view.safeAreaLayoutGuide) + make.top.equalTo(logoImageView.snp.bottom).offset(13) make.leading.trailing.equalToSuperview() make.height.equalTo(80) } @@ -71,29 +85,29 @@ final class HomeViewController: BaseViewController { // MARK: - Navigation - private func setupNavigationBar() { - let logoImageView = UIImageView(image: EATSSUDesignAsset.Images.mainLogoSmall.image) - navigationItem.titleView = logoImageView - - let rightButton = UIBarButtonItem( - image: EATSSUDesignAsset.Images.myPageIcon.image, - style: .plain, - target: self, - action: #selector(didTapRightBarButton) - ) - rightButton.tintColor = EATSSUDesignAsset.Color.Main.primary.color - navigationItem.rightBarButtonItem = rightButton - navigationController?.isNavigationBarHidden = false - } - - @objc - private func didTapRightBarButton() { - if RealmService.shared.isAccessTokenPresent() { - navigateToMyPage() - } else { - presentLoginAlert() - } - } +// private func setupNavigationBar() { +// let logoImageView = UIImageView(image: EATSSUDesignAsset.Images.mainLogoSmall.image) +// navigationItem.titleView = logoImageView +// +// let rightButton = UIBarButtonItem( +// image: EATSSUDesignAsset.Images.myPageIcon.image, +// style: .plain, +// target: self, +// action: #selector(didTapRightBarButton) +// ) +// rightButton.tintColor = EATSSUDesignAsset.Color.Main.primary.color +// navigationItem.rightBarButtonItem = rightButton +// navigationController?.isNavigationBarHidden = false +// } +// +// @objc +// private func didTapRightBarButton() { +// if RealmService.shared.isAccessTokenPresent() { +// navigateToMyPage() +// } else { +// presentLoginAlert() +// } +// } private func navigateToMyPage() { let myPageVC = MyPageViewController() diff --git a/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift b/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift new file mode 100644 index 00000000..13420e0c --- /dev/null +++ b/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift @@ -0,0 +1,52 @@ +// +// RootTabBarViewController.swift +// EATSSU-DEV +// +// Created by 황상환 on 6/20/25. +// + +import UIKit + +final class RootTabBarViewController: UITabBarController { + + override func viewDidLoad() { + super.viewDidLoad() + setupTabBar() + setupTabBarStyle() + view.backgroundColor = .white + } + + private func setupTabBar() { + let homeVC = HomeViewController() + let mapVC = UINavigationController(rootViewController: HomeViewController()) + let myVC = UINavigationController(rootViewController: MyPageViewController()) + + homeVC.tabBarItem = UITabBarItem(title: "학식", image: UIImage(systemName: "fork.knife"), tag: 0) + mapVC.tabBarItem = UITabBarItem(title: "지도", image: UIImage(systemName: "map.fill"), tag: 1) + myVC.tabBarItem = UITabBarItem(title: "마이", image: UIImage(systemName: "person.fill"), tag: 2) + + viewControllers = [homeVC, mapVC, myVC] + } + + private func setupTabBarStyle() { + let appearance = UITabBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = .white + + tabBar.layer.cornerRadius = 20 + tabBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + tabBar.layer.masksToBounds = false + + tabBar.layer.shadowColor = UIColor.black.cgColor + tabBar.layer.shadowOpacity = 0.1 + tabBar.layer.shadowOffset = CGSize(width: 0, height: -2) + tabBar.layer.shadowRadius = 10 + + tabBar.standardAppearance = appearance + if #available(iOS 15.0, *) { + tabBar.scrollEdgeAppearance = appearance + } + } + +} + From 06debfd2aa1013ba6fe41cdece454aaede2a8715 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 21 Jun 2025 23:56:39 +0900 Subject: [PATCH 02/52] =?UTF-8?q?[#234]=ED=95=98=EB=8B=A8=20=ED=83=AD?= =?UTF-8?q?=EB=B0=94=20UI=202=EC=B0=A8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/LoginViewController.swift | 4 +- .../CustomTabBarContainerController.swift | 61 ++++++++++ .../Presentation/CustomTabBarView.swift | 107 ++++++++++++++++++ .../RootTabBarViewController.swift | 45 ++++++-- 4 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift create mode 100644 EATSSU/App/Sources/Presentation/CustomTabBarView.swift diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index f360e805..43f79c64 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift @@ -95,12 +95,12 @@ final class LoginViewController: BaseViewController { } private func changeIntoHomeViewController() { - let rootTabBar = RootTabBarViewController() + let customTabVC = CustomTabBarContainerController() guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) else { return } - keyWindow.replaceRootViewController(rootTabBar) + keyWindow.replaceRootViewController(customTabVC) } /// 닉네임 설정이 필요한지 확인 후, 필요하면 닉네임 설정 화면으로, 아니면 홈 화면으로 이동한다. diff --git a/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift new file mode 100644 index 00000000..0333b3c0 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift @@ -0,0 +1,61 @@ +// +// CustomTabBarContainerController.swift +// EATSSU-DEV +// +// Created by 황상환 on 6/21/25. +// + +import UIKit + +final class CustomTabBarContainerController: UIViewController { + + private let tabBarView = CustomTabBarView() + private let viewControllers: [UIViewController] = [ + HomeViewController(), + HomeViewController(), + MyPageViewController() + ] + private var currentIndex = 0 + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + setupTabBar() + switchToViewController(at: currentIndex) + } + + private func setupTabBar() { + view.addSubview(tabBarView) + tabBarView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(view.snp.bottom) + $0.height.equalTo(74) + } + + tabBarView.buttonTapped = { [weak self] index in + self?.switchToViewController(at: index) + } + } + + + private func switchToViewController(at index: Int) { + let selectedVC = viewControllers[index] + + // remove previous + children.forEach { child in + child.view.removeFromSuperview() + child.removeFromParent() + } + + addChild(selectedVC) + view.insertSubview(selectedVC.view, belowSubview: tabBarView) + selectedVC.view.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.bottom.equalTo(tabBarView.snp.top) + } + selectedVC.didMove(toParent: self) + + tabBarView.setSelectedIndex(index) + currentIndex = index + } +} diff --git a/EATSSU/App/Sources/Presentation/CustomTabBarView.swift b/EATSSU/App/Sources/Presentation/CustomTabBarView.swift new file mode 100644 index 00000000..900c939c --- /dev/null +++ b/EATSSU/App/Sources/Presentation/CustomTabBarView.swift @@ -0,0 +1,107 @@ +// +// CustomTabBarView.swift +// EATSSU-DEV +// +// Created by 황상환 on 6/21/25. +// + +import UIKit + +import EATSSUDesign + +final class CustomTabBarView: UIView { + + var buttonTapped: ((Int) -> Void)? + + private let buttons: [UIButton] = { + let titles = ["학식", "지도", "마이"] + let images = ["fork.knife", "map.fill", "person.fill"] + + return zip(titles, images).enumerated().map { index, pair in + var config = UIButton.Configuration.plain() + config.title = pair.0 + config.image = UIImage( + systemName: pair.1, + withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular) + ) + config.imagePlacement = .top + config.imagePadding = 4 + config.baseForegroundColor = .gray + + let button = UIButton(configuration: config, primaryAction: nil) + button.tag = index + + // Pretendard 글꼴 설정 (regular 11pt) + let font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 12) + var attrTitle = AttributedString(pair.0) + attrTitle.font = font + attrTitle.foregroundColor = .gray + config.attributedTitle = attrTitle + button.configuration = config + + return button + } + }() + + + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError() + } + + private func setupUI() { + backgroundColor = .white + layer.cornerRadius = 10 + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + layer.masksToBounds = false + + // 그림자 + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.06 + layer.shadowOffset = CGSize(width: 0, height: -6) + layer.shadowRadius = 12 + + // 버튼 스택 + let stack = UIStackView(arrangedSubviews: buttons) + stack.axis = .horizontal + stack.distribution = .fillEqually + stack.alignment = .center + addSubview(stack) + + stack.snp.makeConstraints { $0.edges.equalToSuperview().inset(8) } + + buttons.forEach { button in + button.setTitleColor(.gray, for: .normal) + button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) + } + } + + func setSelectedIndex(_ index: Int) { + for (i, button) in buttons.enumerated() { + let isSelected = i == index + let color: UIColor = isSelected ? EATSSUDesignAsset.Color.Main.primary.color : .gray + + if var config = button.configuration, + let title = config.title { + var attrTitle = AttributedString(title) + attrTitle.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 11) + attrTitle.foregroundColor = color + config.attributedTitle = attrTitle + + config.baseForegroundColor = color + + button.configuration = config + } + } + } + + @objc private func buttonTapped(_ sender: UIButton) { + setSelectedIndex(sender.tag) + buttonTapped?(sender.tag) + } +} diff --git a/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift b/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift index 13420e0c..4c66c582 100644 --- a/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift +++ b/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift @@ -17,30 +17,61 @@ final class RootTabBarViewController: UITabBarController { } private func setupTabBar() { + // 학식 탭 let homeVC = HomeViewController() + let homeItem = UITabBarItem(title: "학식", image: UIImage(systemName: "fork.knife"), tag: 0) + homeItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -2) + homeItem.imageInsets = UIEdgeInsets(top: -2, left: 0, bottom: 2, right: 0) + homeVC.tabBarItem = homeItem + + // 지도 탭 let mapVC = UINavigationController(rootViewController: HomeViewController()) - let myVC = UINavigationController(rootViewController: MyPageViewController()) + let mapItem = UITabBarItem(title: "지도", image: UIImage(systemName: "map.fill"), tag: 1) + mapItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -2) + mapItem.imageInsets = UIEdgeInsets(top: -2, left: 0, bottom: 2, right: 0) + mapVC.tabBarItem = mapItem - homeVC.tabBarItem = UITabBarItem(title: "학식", image: UIImage(systemName: "fork.knife"), tag: 0) - mapVC.tabBarItem = UITabBarItem(title: "지도", image: UIImage(systemName: "map.fill"), tag: 1) - myVC.tabBarItem = UITabBarItem(title: "마이", image: UIImage(systemName: "person.fill"), tag: 2) + // 마이 탭 + let myVC = UINavigationController(rootViewController: MyPageViewController()) + let myItem = UITabBarItem(title: "마이", image: UIImage(systemName: "person.fill"), tag: 2) + myItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -2) + myItem.imageInsets = UIEdgeInsets(top: -2, left: 0, bottom: 2, right: 0) + myVC.tabBarItem = myItem + // 탭 배열 등록 viewControllers = [homeVC, mapVC, myVC] } + private func setupTabBarStyle() { let appearance = UITabBarAppearance() appearance.configureWithOpaqueBackground() + + // 배경 흰색 appearance.backgroundColor = .white + // 분리선 제거 + appearance.shadowColor = .clear + appearance.shadowImage = UIImage() + + // 선택 상태 색상 (선택됨/비선택) + let itemAppearance = UITabBarItemAppearance() + itemAppearance.selected.iconColor = UIColor.systemTeal + itemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.systemTeal] + itemAppearance.normal.iconColor = UIColor.gray + itemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.gray] + appearance.stackedLayoutAppearance = itemAppearance + + // 탭바 자체 스타일 tabBar.layer.cornerRadius = 20 tabBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] tabBar.layer.masksToBounds = false + // 그림자 효과 tabBar.layer.shadowColor = UIColor.black.cgColor - tabBar.layer.shadowOpacity = 0.1 - tabBar.layer.shadowOffset = CGSize(width: 0, height: -2) - tabBar.layer.shadowRadius = 10 + tabBar.layer.shadowOpacity = 0.06 + tabBar.layer.shadowOffset = CGSize(width: 0, height: -6) + tabBar.layer.shadowRadius = 12 tabBar.standardAppearance = appearance if #available(iOS 15.0, *) { From e4bdfeb0b626765470e04824b6b87722f1dbcf07 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sun, 22 Jun 2025 20:33:55 +0900 Subject: [PATCH 03/52] =?UTF-8?q?[#234]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=95=88=ED=96=88=EC=9D=84=20=EB=95=8C=20alert=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomTabBarContainerController.swift | 38 ++++++++- .../Presentation/CustomTabBarView.swift | 5 +- .../HomeRestaurantViewController.swift | 22 +++++ .../ViewController/HomeViewController.swift | 55 ------------ .../RootTabBarViewController.swift | 83 ------------------- 5 files changed, 58 insertions(+), 145 deletions(-) delete mode 100644 EATSSU/App/Sources/Presentation/RootTabBarViewController.swift diff --git a/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift index 0333b3c0..6fc2140f 100644 --- a/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift @@ -33,15 +33,21 @@ final class CustomTabBarContainerController: UIViewController { } tabBarView.buttonTapped = { [weak self] index in - self?.switchToViewController(at: index) + guard let self = self else { return } + + // 로그인 요청 띄우기 + if index == 2, RealmService.shared.isAccessTokenPresent() == false { + self.presentLoginAlert() + return + } + + self.switchToViewController(at: index) } } - private func switchToViewController(at index: Int) { let selectedVC = viewControllers[index] - // remove previous children.forEach { child in child.view.removeFromSuperview() child.removeFromParent() @@ -58,4 +64,30 @@ final class CustomTabBarContainerController: UIViewController { tabBarView.setSelectedIndex(index) currentIndex = index } + + private func presentLoginAlert() { + let alert = UIAlertController(title: "로그인이 필요한 서비스입니다", + message: "로그인 하시겠습니까?", + preferredStyle: .alert) + let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in + self?.navigateToLogin() + } + let cancelAction = UIAlertAction(title: "취소", style: .cancel) { [weak self] _ in + guard let self = self else { return } + self.tabBarView.setSelectedIndex(self.currentIndex) + } + alert.addAction(confirmAction) + alert.addAction(cancelAction) + present(alert, animated: true, completion: nil) + } + + private func navigateToLogin() { + let loginVC = LoginViewController() + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let sceneDelegate = windowScene.delegate as? SceneDelegate, + let window = sceneDelegate.window { + window.replaceRootViewController(loginVC) + } + } + } diff --git a/EATSSU/App/Sources/Presentation/CustomTabBarView.swift b/EATSSU/App/Sources/Presentation/CustomTabBarView.swift index 900c939c..02c9c2f8 100644 --- a/EATSSU/App/Sources/Presentation/CustomTabBarView.swift +++ b/EATSSU/App/Sources/Presentation/CustomTabBarView.swift @@ -31,7 +31,6 @@ final class CustomTabBarView: UIView { let button = UIButton(configuration: config, primaryAction: nil) button.tag = index - // Pretendard 글꼴 설정 (regular 11pt) let font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 12) var attrTitle = AttributedString(pair.0) attrTitle.font = font @@ -43,8 +42,6 @@ final class CustomTabBarView: UIView { } }() - - override init(frame: CGRect) { super.init(frame: frame) setupUI() @@ -63,7 +60,7 @@ final class CustomTabBarView: UIView { // 그림자 layer.shadowColor = UIColor.black.cgColor layer.shadowOpacity = 0.06 - layer.shadowOffset = CGSize(width: 0, height: -6) + layer.shadowOffset = CGSize(width: 0, height: -3) layer.shadowRadius = 12 // 버튼 스택 diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift index 88082950..ee0bbf81 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeRestaurantViewController.swift @@ -217,6 +217,11 @@ extension HomeRestaurantViewController: UITableViewDataSource { } private func handleMenuTap(section: Int, menuIndex: Int) { + if RealmService.shared.isAccessTokenPresent() == false { + presentLoginAlert() + return + } + let restaurant = getSectionKey(for: section) var reviewMenuTypeInfo = ReviewMenuTypeInfo(menuType: "", menuID: 0) @@ -237,6 +242,23 @@ extension HomeRestaurantViewController: UITableViewDataSource { navigationController?.pushViewController(reviewViewController, animated: true) delegate?.didDelegateReviewMenuTypeInfo(for: reviewMenuTypeInfo) } + + private func presentLoginAlert() { + let alert = UIAlertController(title: "로그인이 필요한 서비스입니다", + message: "로그인 하시겠습니까?", + preferredStyle: .alert) + let confirm = UIAlertAction(title: "확인", style: .default) { _ in + let loginVC = LoginViewController() + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let sceneDelegate = windowScene.delegate as? SceneDelegate, + let window = sceneDelegate.window { + window.replaceRootViewController(loginVC) + } + } + alert.addAction(confirm) + alert.addAction(UIAlertAction(title: "취소", style: .cancel)) + present(alert, animated: true) + } // 섹션 헤더 뷰 설정 func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index b4d81a72..52e83af2 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -42,7 +42,6 @@ final class HomeViewController: BaseViewController { configureUI() setLayout() registerTabman() -// setupNavigationBar() } override func viewDidAppear(_ animated: Bool) { @@ -83,60 +82,6 @@ final class HomeViewController: BaseViewController { tabmanController.didMove(toParent: self) } - // MARK: - Navigation - -// private func setupNavigationBar() { -// let logoImageView = UIImageView(image: EATSSUDesignAsset.Images.mainLogoSmall.image) -// navigationItem.titleView = logoImageView -// -// let rightButton = UIBarButtonItem( -// image: EATSSUDesignAsset.Images.myPageIcon.image, -// style: .plain, -// target: self, -// action: #selector(didTapRightBarButton) -// ) -// rightButton.tintColor = EATSSUDesignAsset.Color.Main.primary.color -// navigationItem.rightBarButtonItem = rightButton -// navigationController?.isNavigationBarHidden = false -// } -// -// @objc -// private func didTapRightBarButton() { -// if RealmService.shared.isAccessTokenPresent() { -// navigateToMyPage() -// } else { -// presentLoginAlert() -// } -// } - - private func navigateToMyPage() { - let myPageVC = MyPageViewController() - navigationController?.pushViewController(myPageVC, animated: true) - } - - private func presentLoginAlert() { - let alert = UIAlertController(title: "로그인이 필요한 서비스입니다", - message: "로그인 하시겠습니까?", - preferredStyle: .alert) - let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in - self?.navigateToLogin() - } - let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil) - alert.addAction(confirmAction) - alert.addAction(cancelAction) - present(alert, animated: true, completion: nil) - } - - private func navigateToLogin() { - let loginVC = LoginViewController() - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let sceneDelegate = windowScene.delegate as? SceneDelegate, - let window = sceneDelegate.window - { - window.replaceRootViewController(loginVC) - } - } - // MARK: - Firebase private func logFirebaseEvent() { diff --git a/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift b/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift deleted file mode 100644 index 4c66c582..00000000 --- a/EATSSU/App/Sources/Presentation/RootTabBarViewController.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// RootTabBarViewController.swift -// EATSSU-DEV -// -// Created by 황상환 on 6/20/25. -// - -import UIKit - -final class RootTabBarViewController: UITabBarController { - - override func viewDidLoad() { - super.viewDidLoad() - setupTabBar() - setupTabBarStyle() - view.backgroundColor = .white - } - - private func setupTabBar() { - // 학식 탭 - let homeVC = HomeViewController() - let homeItem = UITabBarItem(title: "학식", image: UIImage(systemName: "fork.knife"), tag: 0) - homeItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -2) - homeItem.imageInsets = UIEdgeInsets(top: -2, left: 0, bottom: 2, right: 0) - homeVC.tabBarItem = homeItem - - // 지도 탭 - let mapVC = UINavigationController(rootViewController: HomeViewController()) - let mapItem = UITabBarItem(title: "지도", image: UIImage(systemName: "map.fill"), tag: 1) - mapItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -2) - mapItem.imageInsets = UIEdgeInsets(top: -2, left: 0, bottom: 2, right: 0) - mapVC.tabBarItem = mapItem - - // 마이 탭 - let myVC = UINavigationController(rootViewController: MyPageViewController()) - let myItem = UITabBarItem(title: "마이", image: UIImage(systemName: "person.fill"), tag: 2) - myItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -2) - myItem.imageInsets = UIEdgeInsets(top: -2, left: 0, bottom: 2, right: 0) - myVC.tabBarItem = myItem - - // 탭 배열 등록 - viewControllers = [homeVC, mapVC, myVC] - } - - - private func setupTabBarStyle() { - let appearance = UITabBarAppearance() - appearance.configureWithOpaqueBackground() - - // 배경 흰색 - appearance.backgroundColor = .white - - // 분리선 제거 - appearance.shadowColor = .clear - appearance.shadowImage = UIImage() - - // 선택 상태 색상 (선택됨/비선택) - let itemAppearance = UITabBarItemAppearance() - itemAppearance.selected.iconColor = UIColor.systemTeal - itemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.systemTeal] - itemAppearance.normal.iconColor = UIColor.gray - itemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.gray] - appearance.stackedLayoutAppearance = itemAppearance - - // 탭바 자체 스타일 - tabBar.layer.cornerRadius = 20 - tabBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - tabBar.layer.masksToBounds = false - - // 그림자 효과 - tabBar.layer.shadowColor = UIColor.black.cgColor - tabBar.layer.shadowOpacity = 0.06 - tabBar.layer.shadowOffset = CGSize(width: 0, height: -6) - tabBar.layer.shadowRadius = 12 - - tabBar.standardAppearance = appearance - if #available(iOS 15.0, *) { - tabBar.scrollEdgeAppearance = appearance - } - } - -} - From 2d74843d41f77952e15a87071b4698c57504acb7 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sun, 22 Jun 2025 20:41:15 +0900 Subject: [PATCH 04/52] =?UTF-8?q?[#234]=20tabBar=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20Base=20=EC=BD=94=EB=93=9C=EB=A1=9C=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 --- .../CustomTabBarContainerController.swift | 25 ++++++++-------- .../Presentation/CustomTabBarView.swift | 29 ++++++------------- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift index 6fc2140f..af66caf7 100644 --- a/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift @@ -7,7 +7,7 @@ import UIKit -final class CustomTabBarContainerController: UIViewController { +final class CustomTabBarContainerController: BaseViewController { private let tabBarView = CustomTabBarView() private let viewControllers: [UIViewController] = [ @@ -19,23 +19,15 @@ final class CustomTabBarContainerController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .white - setupTabBar() switchToViewController(at: currentIndex) } - private func setupTabBar() { + override func configureUI() { view.addSubview(tabBarView) - tabBarView.snp.makeConstraints { - $0.leading.trailing.equalToSuperview() - $0.bottom.equalTo(view.snp.bottom) - $0.height.equalTo(74) - } tabBarView.buttonTapped = { [weak self] index in guard let self = self else { return } - // 로그인 요청 띄우기 if index == 2, RealmService.shared.isAccessTokenPresent() == false { self.presentLoginAlert() return @@ -45,6 +37,14 @@ final class CustomTabBarContainerController: UIViewController { } } + override func setLayout() { + tabBarView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.bottom.equalTo(view.snp.bottom) + $0.height.equalTo(74) + } + } + private func switchToViewController(at index: Int) { let selectedVC = viewControllers[index] @@ -64,7 +64,7 @@ final class CustomTabBarContainerController: UIViewController { tabBarView.setSelectedIndex(index) currentIndex = index } - + private func presentLoginAlert() { let alert = UIAlertController(title: "로그인이 필요한 서비스입니다", message: "로그인 하시겠습니까?", @@ -78,7 +78,7 @@ final class CustomTabBarContainerController: UIViewController { } alert.addAction(confirmAction) alert.addAction(cancelAction) - present(alert, animated: true, completion: nil) + present(alert, animated: true) } private func navigateToLogin() { @@ -89,5 +89,4 @@ final class CustomTabBarContainerController: UIViewController { window.replaceRootViewController(loginVC) } } - } diff --git a/EATSSU/App/Sources/Presentation/CustomTabBarView.swift b/EATSSU/App/Sources/Presentation/CustomTabBarView.swift index 02c9c2f8..79ddf887 100644 --- a/EATSSU/App/Sources/Presentation/CustomTabBarView.swift +++ b/EATSSU/App/Sources/Presentation/CustomTabBarView.swift @@ -9,7 +9,7 @@ import UIKit import EATSSUDesign -final class CustomTabBarView: UIView { +final class CustomTabBarView: BaseUIView { var buttonTapped: ((Int) -> Void)? @@ -42,28 +42,24 @@ final class CustomTabBarView: UIView { } }() - override init(frame: CGRect) { - super.init(frame: frame) - setupUI() - } - - required init?(coder: NSCoder) { - fatalError() - } - - private func setupUI() { + override func configureUI() { backgroundColor = .white layer.cornerRadius = 10 layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] layer.masksToBounds = false - // 그림자 layer.shadowColor = UIColor.black.cgColor layer.shadowOpacity = 0.06 layer.shadowOffset = CGSize(width: 0, height: -3) layer.shadowRadius = 12 - // 버튼 스택 + buttons.forEach { button in + button.setTitleColor(.gray, for: .normal) + button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) + } + } + + override func setLayout() { let stack = UIStackView(arrangedSubviews: buttons) stack.axis = .horizontal stack.distribution = .fillEqually @@ -71,11 +67,6 @@ final class CustomTabBarView: UIView { addSubview(stack) stack.snp.makeConstraints { $0.edges.equalToSuperview().inset(8) } - - buttons.forEach { button in - button.setTitleColor(.gray, for: .normal) - button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) - } } func setSelectedIndex(_ index: Int) { @@ -89,9 +80,7 @@ final class CustomTabBarView: UIView { attrTitle.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 11) attrTitle.foregroundColor = color config.attributedTitle = attrTitle - config.baseForegroundColor = color - button.configuration = config } } From 4009621c05f25f9386d3c832686c26b2962cac68 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:24:33 +0900 Subject: [PATCH 05/52] =?UTF-8?q?[#234]=20HomeViewController=20=EB=84=A4?= =?UTF-8?q?=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=B2=84=EA=B7=B8=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 --- .../Presentation/CustomTabBarContainerController.swift | 6 +++--- .../Home/ViewController/HomeViewController.swift | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift index af66caf7..6738a291 100644 --- a/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift @@ -11,9 +11,9 @@ final class CustomTabBarContainerController: BaseViewController { private let tabBarView = CustomTabBarView() private let viewControllers: [UIViewController] = [ - HomeViewController(), - HomeViewController(), - MyPageViewController() + UINavigationController(rootViewController: HomeViewController()), + UINavigationController(rootViewController: HomeViewController()), + UINavigationController(rootViewController: MyPageViewController()) ] private var currentIndex = 0 diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index 52e83af2..b85d0fb4 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -48,6 +48,16 @@ final class HomeViewController: BaseViewController { super.viewDidAppear(animated) logFirebaseEvent() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(true, animated: animated) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationController?.setNavigationBarHidden(false, animated: animated) + } // MARK: - UI Configuration From 5c43819bce5fef16123ee5dc8dcc4230b1997e65 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:45:14 +0900 Subject: [PATCH 06/52] =?UTF-8?q?[#234]=20=EB=84=A4=EC=9D=B4=EB=B2=84=20?= =?UTF-8?q?=EB=A7=B5=201=EC=B0=A8=20=EC=97=B0=EB=8F=99=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/NaverMapTestViewController.swift | 34 +++++++++++++++++++ .../CustomTabBarContainerController.swift | 2 +- .../{ => TabBar}/CustomTabBarView.swift | 0 .../Utility/Application/AppDelegate.swift | 13 +++++++ EATSSU/Project.swift | 7 ++-- Tuist/Package.resolved | 20 ++++++++++- Tuist/Package.swift | 5 +++ 7 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/Map/NaverMapTestViewController.swift rename EATSSU/App/Sources/Presentation/{ => TabBar}/CustomTabBarContainerController.swift (97%) rename EATSSU/App/Sources/Presentation/{ => TabBar}/CustomTabBarView.swift (100%) diff --git a/EATSSU/App/Sources/Presentation/Map/NaverMapTestViewController.swift b/EATSSU/App/Sources/Presentation/Map/NaverMapTestViewController.swift new file mode 100644 index 00000000..9d4c72e5 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Map/NaverMapTestViewController.swift @@ -0,0 +1,34 @@ +// +// NaverMapTestViewController.swift +// EATSSU-DEV +// +// Created by 황상환 on 6/24/25. +// + +import UIKit +import NMapsMap + +final class NaverMapTestViewController: UIViewController { + + private let mapView = NMFNaverMapView() + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + setupMapView() + } + + private func setupMapView() { + view.addSubview(mapView) + mapView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + mapView.topAnchor.constraint(equalTo: view.topAnchor), + mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + } +} + diff --git a/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift similarity index 97% rename from EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift rename to EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 6738a291..45957db4 100644 --- a/EATSSU/App/Sources/Presentation/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -12,7 +12,7 @@ final class CustomTabBarContainerController: BaseViewController { private let tabBarView = CustomTabBarView() private let viewControllers: [UIViewController] = [ UINavigationController(rootViewController: HomeViewController()), - UINavigationController(rootViewController: HomeViewController()), + UINavigationController(rootViewController: NaverMapTestViewController()), UINavigationController(rootViewController: MyPageViewController()) ] private var currentIndex = 0 diff --git a/EATSSU/App/Sources/Presentation/CustomTabBarView.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift similarity index 100% rename from EATSSU/App/Sources/Presentation/CustomTabBarView.swift rename to EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift diff --git a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift index 4fa1fc78..49ddd94f 100644 --- a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift @@ -10,6 +10,7 @@ import UIKit import Firebase import KakaoSDKCommon +import NMapsMap @main class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { @@ -22,6 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD handleAppleSignIn() initializeKakaoSDK() setupDebugConfigurations() + configureNaverMapAuth() UNUserNotificationCenter.current().delegate = self return true @@ -41,6 +43,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD LaunchSourceManager.shared.setSource(.localNotification) completionHandler() } + + // 네이버 맵 키 설정 + private func configureNaverMapAuth() { + if let key = Bundle.main.infoDictionary?["NAVER_CLIENT_ID"] as? String { + NMFAuthManager.shared().ncpKeyId = key + print("NaverMap Client ID 설정 완료: \(key)") + } else { + print("NAVER_CLIENT_ID 못 찾음") + } + } + // MARK: - Private Methods diff --git a/EATSSU/Project.swift b/EATSSU/Project.swift index 192f874a..9ab1a7aa 100644 --- a/EATSSU/Project.swift +++ b/EATSSU/Project.swift @@ -37,6 +37,7 @@ let appInfoPlist: InfoPlist = .extendingDefault(with: [ ], // 사용 국가 지정 "CFBundleDevelopmentRegion": "ko", + "NAVER_CLIENT_ID": "$(NAVER_CLIENT_ID)", ]) let widgetInfoPlist: InfoPlist = .extendingDefault(with: [ @@ -99,7 +100,8 @@ let project = Project( .external(name: "KakaoSDKUser"), .external(name: "KakaoSDKCommon"), .external(name: "KakaoSDKTalk"), - + .external(name: "NMapsMap"), + // EATSSU 내장 라이브러리 .project(target: "EATSSUDesign", path: .relativeToRoot("../EATSSUDesign"), condition: .none), ], @@ -134,7 +136,8 @@ let project = Project( .external(name: "KakaoSDKUser"), .external(name: "KakaoSDKCommon"), .external(name: "KakaoSDKTalk"), - + .external(name: "NMapsMap"), + // EATSSU 내장 라이브러리 .project(target: "EATSSUDesign", path: .relativeToRoot("../EATSSUDesign"), condition: .none), ], diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index 7ceaadd8..08824c43 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "86c28210c9f80f904576c16e2ec3d535e73b521baf7974cd5ea3fca1e60e6190", + "originHash" : "9d69386e6ea6495a44e2469f723c08a7f59b16773d9d210eb24af5de456a00e4", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -208,6 +208,24 @@ "version" : "5.7.1" } }, + { + "identity" : "spm-nmapsgeometry", + "kind" : "remoteSourceControl", + "location" : "https://github.com/navermaps/SPM-NMapsGeometry.git", + "state" : { + "revision" : "436d5e2e684f557faf5ef5862fd6633a42d7af11", + "version" : "1.0.2" + } + }, + { + "identity" : "spm-nmapsmap", + "kind" : "remoteSourceControl", + "location" : "https://github.com/navermaps/SPM-NMapsMap", + "state" : { + "revision" : "13a6d280a57c4ebab8320e2d5bf3ce89adacf95e", + "version" : "3.22.0" + } + }, { "identity" : "swift-protobuf", "kind" : "remoteSourceControl", diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 83137b14..df8b3594 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -33,6 +33,9 @@ import PackageDescription // RxSwift "RxSwift": .framework, + + // Naver Maps + "NMapsMap": .framework, ] ) #endif @@ -51,5 +54,7 @@ let package = Package( .package(url: "https://github.com/google/GoogleAppMeasurement", from: "11.1.0"), .package(url: "https://github.com/realm/realm-swift", from: "20.0.0"), .package(url: "https://github.com/ReactiveX/RxSwift", from: "6.7.1"), + .package(url: "https://github.com/navermaps/SPM-NMapsMap", from: "3.20.0"), + ] ) From 470e95b64062481c8ecc0e5f129c323ca0abeefa Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 25 Jun 2025 00:12:26 +0900 Subject: [PATCH 07/52] =?UTF-8?q?[#234]=20map=20UI=201=EC=B0=A8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Map/MainMapView 2.swift | 127 ++++++++++++++++++ .../Presentation/Map/MainMapView.swift | 118 ++++++++++++++++ .../Map/MainMapViewController.swift | 58 ++++++++ .../Map/NaverMapTestViewController.swift | 34 ----- .../CustomTabBarContainerController.swift | 2 +- 5 files changed, 304 insertions(+), 35 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/Map/MainMapView 2.swift create mode 100644 EATSSU/App/Sources/Presentation/Map/MainMapView.swift create mode 100644 EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift delete mode 100644 EATSSU/App/Sources/Presentation/Map/NaverMapTestViewController.swift diff --git a/EATSSU/App/Sources/Presentation/Map/MainMapView 2.swift b/EATSSU/App/Sources/Presentation/Map/MainMapView 2.swift new file mode 100644 index 00000000..5157d501 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Map/MainMapView 2.swift @@ -0,0 +1,127 @@ +// +// MainMapView 2.swift +// EATSSU +// +// Created by 황상환 on 6/24/25. +// + + +import UIKit +import NMapsMap +import SnapKit +import EATSSUDesign + +final class MainMapView: UIView { + + let mapView = NMFNaverMapView() + let titleLabel = UILabel() + let toggleBackgroundView = UIView() + let wholeButton = UIButton(type: .system) + let myOnlyButton = UIButton(type: .system) + let heartButton = UIButton(type: .system) + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError() + } + + private func setupViews() { + backgroundColor = .white + + mapView.showZoomControls = false + addSubview(mapView) + + titleLabel.text = "제휴 지도" + titleLabel.textColor = .black + titleLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 20) + titleLabel.textAlignment = .center + titleLabel.backgroundColor = .white + titleLabel.layer.shadowColor = UIColor.black.cgColor + titleLabel.layer.shadowOpacity = 0.1 + titleLabel.layer.shadowOffset = CGSize(width: 0, height: 2) + titleLabel.layer.shadowRadius = 4 + addSubview(titleLabel) + + toggleBackgroundView.layer.cornerRadius = 20 + toggleBackgroundView.layer.borderWidth = 1 + toggleBackgroundView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + toggleBackgroundView.backgroundColor = .white + addSubview(toggleBackgroundView) + + [wholeButton, myOnlyButton].forEach { + $0.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + $0.layer.cornerRadius = 14 + $0.clipsToBounds = true + $0.backgroundColor = .clear + $0.setTitleColor(.label, for: .normal) + } + + wholeButton.setTitle("전체", for: .normal) + myOnlyButton.setTitle("내 제휴", for: .normal) + + toggleBackgroundView.addSubview(wholeButton) + toggleBackgroundView.addSubview(myOnlyButton) + + let heartImage = UIImage(systemName: "heart")? + .withConfiguration(UIImage.SymbolConfiguration(pointSize: 13, weight: .regular)) + heartButton.setImage(heartImage, for: .normal) + heartButton.tintColor = EATSSUDesignAsset.Color.Red.error.color + heartButton.backgroundColor = .white + heartButton.layer.cornerRadius = 20 + heartButton.layer.shadowColor = UIColor.black.cgColor + heartButton.layer.shadowOpacity = 0.1 + heartButton.layer.shadowRadius = 4 + heartButton.layer.shadowOffset = CGSize(width: 0, height: 2) + addSubview(heartButton) + } + + private func setupLayout() { + mapView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide.snp.top) + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(60) + } + + toggleBackgroundView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(12) + $0.centerX.equalToSuperview() + $0.width.equalTo(180) + $0.height.equalTo(40) + } + + wholeButton.snp.makeConstraints { + $0.top.bottom.equalToSuperview().inset(4) + $0.leading.equalToSuperview().inset(4) + $0.width.equalTo(80) + } + + myOnlyButton.snp.makeConstraints { + $0.top.bottom.equalToSuperview().inset(4) + $0.trailing.equalToSuperview().inset(4) + $0.width.equalTo(80) + } + + heartButton.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(16) + $0.top.equalTo(titleLabel.snp.bottom).offset(12) + $0.size.equalTo(40) + } + } + + func setButtonSelection(isWholeSelected: Bool) { + wholeButton.backgroundColor = isWholeSelected ? EATSSUDesignAsset.Color.Main.primary.color : .clear + wholeButton.setTitleColor(isWholeSelected ? .white : .label, for: .normal) + + myOnlyButton.backgroundColor = isWholeSelected ? .clear : EATSSUDesignAsset.Color.Main.primary.color + myOnlyButton.setTitleColor(isWholeSelected ? .label : .white, for: .normal) + } +} diff --git a/EATSSU/App/Sources/Presentation/Map/MainMapView.swift b/EATSSU/App/Sources/Presentation/Map/MainMapView.swift new file mode 100644 index 00000000..dfdc1dda --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Map/MainMapView.swift @@ -0,0 +1,118 @@ +// +// MainMapView.swift +// EATSSU-DEV +// +// Created by 황상환 on 6/24/25. +// + +import UIKit +import NMapsMap +import SnapKit +import EATSSUDesign + +final class MainMapView: UIView { + + let mapView = NMFNaverMapView() + let toggleBackgroundView = UIView() + let wholeButton = UIButton(type: .system) + let myOnlyButton = UIButton(type: .system) + let heartButton = UIButton(type: .system) + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError() + } + + private func setup() { + backgroundColor = .white + + mapView.showZoomControls = false + addSubview(mapView) + + toggleBackgroundView.layer.cornerRadius = 20 + toggleBackgroundView.layer.borderWidth = 1 + toggleBackgroundView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + toggleBackgroundView.backgroundColor = .white + addSubview(toggleBackgroundView) + + [wholeButton, myOnlyButton].forEach { + $0.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + $0.layer.cornerRadius = 14 + $0.clipsToBounds = true + $0.backgroundColor = .clear + $0.setTitleColor(.label, for: .normal) + } + + wholeButton.setTitle("전체", for: .normal) + myOnlyButton.setTitle("내 제휴", for: .normal) + selectWhole(true) // 초기 선택 상태 + toggleBackgroundView.addSubview(wholeButton) + toggleBackgroundView.addSubview(myOnlyButton) + + let heartImage = UIImage(systemName: "heart")? + .withConfiguration(UIImage.SymbolConfiguration(pointSize: 13, weight: .regular)) + heartButton.setImage(heartImage, for: .normal) + heartButton.tintColor = EATSSUDesignAsset.Color.Red.error.color + heartButton.backgroundColor = .white + heartButton.layer.cornerRadius = 20 + heartButton.layer.shadowColor = UIColor.black.cgColor + heartButton.layer.shadowOpacity = 0.1 + heartButton.layer.shadowRadius = 4 + heartButton.layer.shadowOffset = CGSize(width: 0, height: 2) + addSubview(heartButton) + } + + private func setLayout() { + mapView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + toggleBackgroundView.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(12) + $0.centerX.equalToSuperview() + $0.width.equalTo(180) + $0.height.equalTo(40) + } + + wholeButton.snp.makeConstraints { + $0.top.bottom.equalToSuperview().inset(4) + $0.leading.equalToSuperview().inset(4) + $0.width.equalTo(80) + } + + myOnlyButton.snp.makeConstraints { + $0.top.bottom.equalToSuperview().inset(4) + $0.trailing.equalToSuperview().inset(4) + $0.width.equalTo(80) + } + + heartButton.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(16) + $0.top.equalTo(safeAreaLayoutGuide).offset(12) + $0.size.equalTo(40) + } + } + + + func selectWhole(_ isSelected: Bool) { + if isSelected { + wholeButton.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color + wholeButton.setTitleColor(.white, for: .normal) + + myOnlyButton.backgroundColor = .clear + myOnlyButton.setTitleColor(.label, for: .normal) + } else { + wholeButton.backgroundColor = .clear + wholeButton.setTitleColor(.label, for: .normal) + + myOnlyButton.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color + myOnlyButton.setTitleColor(.white, for: .normal) + } + } + +} diff --git a/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift new file mode 100644 index 00000000..bbd26b1d --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift @@ -0,0 +1,58 @@ +// +// MainMapViewController.swift +// EATSSU-DEV +// +// Created by 황상환 on 6/24/25. +// + +import UIKit +import NMapsMap + +import EATSSUDesign + +final class MainMapViewController: UIViewController { + + private let mainView = MainMapView() + + override func loadView() { + self.view = mainView + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "제휴 지도" + + // 배경색 채우기 + let navBarAppearance = UINavigationBarAppearance() + navBarAppearance.configureWithOpaqueBackground() + navBarAppearance.backgroundColor = .white // 원하는 색상 + navBarAppearance.titleTextAttributes = [ + .foregroundColor: UIColor.black, + .font: EATSSUDesignFontFamily.Pretendard.bold.font(size: 16) + ] + + navigationController?.navigationBar.standardAppearance = navBarAppearance + navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance + navigationController?.navigationBar.compactAppearance = navBarAppearance + + mainView.wholeButton.addTarget(self, action: #selector(didTapWhole), for: .touchUpInside) + mainView.myOnlyButton.addTarget(self, action: #selector(didTapMyOnly), for: .touchUpInside) + mainView.heartButton.addTarget(self, action: #selector(didTapHeart), for: .touchUpInside) + + } + + @objc private func didTapWhole() { + mainView.selectWhole(true) + print("전체 보기") + } + + @objc private func didTapMyOnly() { + mainView.selectWhole(false) + print("내 제휴 보기") + } + + @objc private func didTapHeart() { + print("하트 버튼 클릭됨") + } +} diff --git a/EATSSU/App/Sources/Presentation/Map/NaverMapTestViewController.swift b/EATSSU/App/Sources/Presentation/Map/NaverMapTestViewController.swift deleted file mode 100644 index 9d4c72e5..00000000 --- a/EATSSU/App/Sources/Presentation/Map/NaverMapTestViewController.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// NaverMapTestViewController.swift -// EATSSU-DEV -// -// Created by 황상환 on 6/24/25. -// - -import UIKit -import NMapsMap - -final class NaverMapTestViewController: UIViewController { - - private let mapView = NMFNaverMapView() - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - setupMapView() - } - - private func setupMapView() { - view.addSubview(mapView) - mapView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - mapView.topAnchor.constraint(equalTo: view.topAnchor), - mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - } -} - diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 45957db4..f0c29daf 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -12,7 +12,7 @@ final class CustomTabBarContainerController: BaseViewController { private let tabBarView = CustomTabBarView() private let viewControllers: [UIViewController] = [ UINavigationController(rootViewController: HomeViewController()), - UINavigationController(rootViewController: NaverMapTestViewController()), + UINavigationController(rootViewController: MainMapViewController()), UINavigationController(rootViewController: MyPageViewController()) ] private var currentIndex = 0 From d46417e20c1a527b3828394d8379903540000982 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 25 Jun 2025 18:55:21 +0900 Subject: [PATCH 08/52] =?UTF-8?q?[#234]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=201=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Auth/View/SetNickNameView.swift | 127 ++++++++++++++++-- .../Presentation/Map/MainMapView.swift | 2 +- .../Map/MainMapViewController.swift | 2 +- 3 files changed, 117 insertions(+), 14 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift index 738592d3..f8db47dc 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift @@ -18,11 +18,11 @@ final class SetNickNameView: BaseUIView { // MARK: - UI Components + // 닉네임 설정 private let nickNameLabel: UILabel = { let label = UILabel() - label.text = "EAT-SSU에서 사용할\n닉네임을 설정해 주세요" - label.numberOfLines = 2 - label.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 18) + label.text = "닉네임 설정" + label.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) return label }() @@ -54,9 +54,75 @@ final class SetNickNameView: BaseUIView { stackView.spacing = 8.0 return stackView }() + + // 소속 설정 + private let affiliationLabel: UILabel = { + let label = UILabel() + label.text = "소속 설정" + label.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + return label + }() + + public let collegeSelectButton: UIButton = { + let button = UIButton() + button.setTitle("단과대", for: .normal) + button.setTitleColor(.black, for: .normal) + button.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + button.contentHorizontalAlignment = .left + button.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color + button.layer.cornerRadius = 8 + button.layer.borderWidth = 1 + button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + return button + }() + + public let departmentSelectButton: UIButton = { + let button = UIButton() + button.setTitle("학과", for: .normal) + button.setTitleColor(.black, for: .normal) + button.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + button.contentHorizontalAlignment = .left + button.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color + button.layer.cornerRadius = 8 + button.layer.borderWidth = 1 + button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + return button + }() + + private lazy var affiliationStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [ + collegeSelectButton, + departmentSelectButton + ]) + stackView.axis = .vertical + stackView.spacing = 12 + return stackView + }() + + // 연결 계정 + private let connectedAccountLabel: UILabel = { + let label = UILabel() + label.text = "연결된 계정" + label.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + return label + }() + + public let connectedProviderLabel: UILabel = { + let label = UILabel() + label.text = "APPLE" // 예시, 동적으로 설정 가능 + label.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + return label + }() + + private let appleIconImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = EATSSUDesignAsset.Images.signWithApple.image + imageView.contentMode = .scaleAspectFit + return imageView + }() public var completeSettingNickNameButton: ESButton = { - let button = ESButton(size: .big, title: "완료하기") + let button = ESButton(size: .big, title: "저장하기") button.isEnabled = false return button }() @@ -74,19 +140,24 @@ final class SetNickNameView: BaseUIView { addSubviews( nickNameLabel, setNickNameStackView, - completeSettingNickNameButton, - nicknameDoubleCheckButton + nicknameDoubleCheckButton, + affiliationLabel, + affiliationStackView, + connectedAccountLabel, + connectedProviderLabel, + appleIconImageView, + completeSettingNickNameButton ) } override func setLayout() { nickNameLabel.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide).offset(20) - $0.leading.equalToSuperview().inset(16) + $0.top.equalTo(safeAreaLayoutGuide).offset(16) + $0.leading.equalToSuperview().inset(24) } setNickNameStackView.snp.makeConstraints { - $0.top.equalTo(nickNameLabel.snp.bottom).offset(16) - $0.leading.equalToSuperview().inset(16) + $0.top.equalTo(nickNameLabel.snp.bottom).offset(8) + $0.leading.equalToSuperview().inset(20) $0.trailing.equalTo(nicknameDoubleCheckButton.snp.leading).offset(-5) } nicknameDoubleCheckButton.snp.makeConstraints { @@ -98,10 +169,42 @@ final class SetNickNameView: BaseUIView { inputNickNameTextField.snp.makeConstraints { $0.height.equalTo(48) } + affiliationLabel.snp.makeConstraints { + $0.top.equalTo(setNickNameStackView.snp.bottom).offset(24) + $0.leading.equalToSuperview().inset(24) + } + + affiliationStackView.snp.makeConstraints { + $0.top.equalTo(affiliationLabel.snp.bottom).offset(8) + $0.horizontalEdges.equalToSuperview().inset(24) + } + + collegeSelectButton.snp.makeConstraints { + $0.height.equalTo(48) + } + departmentSelectButton.snp.makeConstraints { + $0.height.equalTo(48) + } + + connectedAccountLabel.snp.makeConstraints { + $0.top.equalTo(affiliationStackView.snp.bottom).offset(24) + $0.leading.equalToSuperview().inset(24) + } + + connectedProviderLabel.snp.makeConstraints { + $0.centerY.equalTo(connectedAccountLabel) + $0.trailing.equalTo(appleIconImageView.snp.leading).offset(-4) + } + + appleIconImageView.snp.makeConstraints { + $0.centerY.equalTo(connectedAccountLabel) + $0.trailing.equalToSuperview().inset(24) + $0.width.height.equalTo(20) + } completeSettingNickNameButton.snp.makeConstraints { - $0.horizontalEdges.equalToSuperview().inset(16) + $0.horizontalEdges.equalToSuperview().inset(24) $0.bottom.equalTo(safeAreaLayoutGuide).inset(26) - $0.height.equalTo(50) + $0.height.equalTo(52) } } diff --git a/EATSSU/App/Sources/Presentation/Map/MainMapView.swift b/EATSSU/App/Sources/Presentation/Map/MainMapView.swift index dfdc1dda..6c53b30b 100644 --- a/EATSSU/App/Sources/Presentation/Map/MainMapView.swift +++ b/EATSSU/App/Sources/Presentation/Map/MainMapView.swift @@ -50,7 +50,7 @@ final class MainMapView: UIView { wholeButton.setTitle("전체", for: .normal) myOnlyButton.setTitle("내 제휴", for: .normal) - selectWhole(true) // 초기 선택 상태 + selectWhole(true) toggleBackgroundView.addSubview(wholeButton) toggleBackgroundView.addSubview(myOnlyButton) diff --git a/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift index bbd26b1d..44b11422 100644 --- a/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift @@ -26,7 +26,7 @@ final class MainMapViewController: UIViewController { // 배경색 채우기 let navBarAppearance = UINavigationBarAppearance() navBarAppearance.configureWithOpaqueBackground() - navBarAppearance.backgroundColor = .white // 원하는 색상 + navBarAppearance.backgroundColor = .white navBarAppearance.titleTextAttributes = [ .foregroundColor: UIColor.black, .font: EATSSUDesignFontFamily.Pretendard.bold.font(size: 16) From fbc511cf1f3c2241575f6c4bbb92ad9b34d1d8e2 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 26 Jun 2025 00:12:18 +0900 Subject: [PATCH 09/52] =?UTF-8?q?[#234]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20UI=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Auth/View/DropDownView.swift | 169 ++++++++++++++++++ .../Auth/View/SetNickNameView.swift | 45 +---- .../SetNickNameViewController.swift | 14 ++ 3 files changed, 191 insertions(+), 37 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift diff --git a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift new file mode 100644 index 00000000..59fc2cbb --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift @@ -0,0 +1,169 @@ +// +// DropDownView.swift +// EATSSU-DEV +// +// Created by 황상환 on 6/25/25. +// + +import UIKit + +import EATSSUDesign + +import SnapKit + +final class DropDownView: UIView { + + // MARK: - Properties + + private var items: [String] + public var onSelect: ((String) -> Void)? + private var isDropdownVisible = false + private var dropdownTableView: UITableView? + + // MARK: - UI Components + + private let button = UIButton(type: .system) + private let arrow = UIImageView(image: UIImage(systemName: "chevron.down")?.withRenderingMode(.alwaysTemplate)) + + // MARK: - Init + + init(title: String, items: [String]) { + self.items = items + super.init(frame: .zero) + setupButton(title: title) + setupLayout() + setupAction() + } + + required init?(coder: NSCoder) { + fatalError() + } + + // MARK: - Setup + + private func setupButton(title: String) { + var config = UIButton.Configuration.filled() + config.attributedTitle = AttributedString( + title, + attributes: AttributeContainer([ + .font: EATSSUDesignFontFamily.Pretendard.regular.font(size: 14), + .foregroundColor: EATSSUDesignAsset.Color.GrayScale.gray700.color + ]) + ) + config.baseBackgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color + config.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 32) + config.titleAlignment = .leading + + button.configuration = config + button.layer.cornerRadius = 12 + button.layer.borderWidth = 1 + button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + button.contentHorizontalAlignment = .leading + } + + private func setupLayout() { + addSubviews(button, arrow) + + button.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.height.equalTo(52) + } + + arrow.snp.makeConstraints { + $0.centerY.equalTo(button) + $0.trailing.equalToSuperview().inset(12) + $0.width.height.equalTo(16) + } + + arrow.tintColor = EATSSUDesignAsset.Color.GrayScale.gray700.color + } + + private func setupAction() { + button.addTarget(self, action: #selector(toggleDropdown), for: .touchUpInside) + } + + // MARK: - Dropdown + + @objc private func toggleDropdown() { + guard let parentView = self.window else { return } + + if isDropdownVisible { + dropdownTableView?.removeFromSuperview() + dropdownTableView = nil + } else { + let tableView = UITableView() + tableView.layer.cornerRadius = 12 + tableView.layer.borderWidth = 1 + tableView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + tableView.layer.shadowColor = UIColor.black.cgColor + tableView.layer.shadowOpacity = 0.1 + tableView.layer.shadowOffset = CGSize(width: 0, height: 2) + tableView.layer.shadowRadius = 4 + tableView.isScrollEnabled = true + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + + parentView.addSubview(tableView) + + let origin = self.convert(self.bounds.origin, to: parentView) + tableView.snp.makeConstraints { + $0.top.equalTo(origin.y + 52) + $0.leading.equalTo(origin.x) + $0.width.equalTo(self.bounds.width) + $0.height.equalTo(min(items.count * 44, 200)) + } + + dropdownTableView = tableView + } + + isDropdownVisible.toggle() + } + + // MARK: - Public + + public func updateItems(_ newItems: [String]) { + items = newItems + dropdownTableView?.reloadData() + } + + public func setTitle(_ title: String) { + var config = button.configuration + config?.attributedTitle = AttributedString( + title, + attributes: AttributeContainer([ + .font: EATSSUDesignFontFamily.Pretendard.regular.font(size: 14), + .foregroundColor: EATSSUDesignAsset.Color.GrayScale.gray700.color + ]) + ) + config?.titleAlignment = .leading + button.configuration = config + } +} + +// MARK: - UITableViewDelegate, UITableViewDataSource + +extension DropDownView: UITableViewDelegate, UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + items.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + var config = cell.defaultContentConfiguration() + config.text = items[indexPath.row] + config.textProperties.color = EATSSUDesignAsset.Color.GrayScale.gray700.color + config.textProperties.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + cell.contentConfiguration = config + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let selected = items[indexPath.row] + setTitle(selected) + onSelect?(selected) + toggleDropdown() + } +} diff --git a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift index f8db47dc..26a4ee2d 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift @@ -15,6 +15,8 @@ final class SetNickNameView: BaseUIView { // MARK: - Properties private var userNickname: String = "" + public let collegeDropDownView = DropDownView(title: "단과대", items: ["인문대", "자연대",]) + public let departmentDropDownView = DropDownView(title: "학과", items: []) // MARK: - UI Components @@ -63,36 +65,10 @@ final class SetNickNameView: BaseUIView { return label }() - public let collegeSelectButton: UIButton = { - let button = UIButton() - button.setTitle("단과대", for: .normal) - button.setTitleColor(.black, for: .normal) - button.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) - button.contentHorizontalAlignment = .left - button.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color - button.layer.cornerRadius = 8 - button.layer.borderWidth = 1 - button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor - return button - }() - - public let departmentSelectButton: UIButton = { - let button = UIButton() - button.setTitle("학과", for: .normal) - button.setTitleColor(.black, for: .normal) - button.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) - button.contentHorizontalAlignment = .left - button.backgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color - button.layer.cornerRadius = 8 - button.layer.borderWidth = 1 - button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor - return button - }() - private lazy var affiliationStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [ - collegeSelectButton, - departmentSelectButton + collegeDropDownView, + departmentDropDownView ]) stackView.axis = .vertical stackView.spacing = 12 @@ -109,7 +85,7 @@ final class SetNickNameView: BaseUIView { public let connectedProviderLabel: UILabel = { let label = UILabel() - label.text = "APPLE" // 예시, 동적으로 설정 가능 + label.text = "APPLE" label.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) return label }() @@ -178,16 +154,11 @@ final class SetNickNameView: BaseUIView { $0.top.equalTo(affiliationLabel.snp.bottom).offset(8) $0.horizontalEdges.equalToSuperview().inset(24) } - - collegeSelectButton.snp.makeConstraints { - $0.height.equalTo(48) - } - departmentSelectButton.snp.makeConstraints { - $0.height.equalTo(48) - } + collegeDropDownView.snp.makeConstraints { $0.height.equalTo(48) } + departmentDropDownView.snp.makeConstraints { $0.height.equalTo(48) } connectedAccountLabel.snp.makeConstraints { - $0.top.equalTo(affiliationStackView.snp.bottom).offset(24) + $0.top.equalTo(affiliationStackView.snp.bottom).offset(40) $0.leading.equalToSuperview().inset(24) } diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index dbbb0b93..e4a09c59 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -18,12 +18,26 @@ final class SetNickNameViewController: BaseViewController { // MARK: - UI Components private let setNickNameView = SetNickNameView() + private func getDepartments(for college: String) -> [String] { + switch college { + case "인문대": return ["국어국문학과", "영어영문학과", "철학과"] + case "자연대": return ["수학과", "물리학과", "화학과"] + default: return [] + } + } + // MARK: - Life Cycles override func viewDidLoad() { super.viewDidLoad() + setNickNameView.collegeDropDownView.onSelect = { [weak self] selectedCollege in + // 학과 목록 업데이트 + let deptList = self?.getDepartments(for: selectedCollege) ?? [] + self?.setNickNameView.departmentDropDownView.updateItems(deptList) + } + dismissKeyboard() } From b1ea93a9b37cb3837201f2c4aad505174a7906ee Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:57:24 +0900 Subject: [PATCH 10/52] =?UTF-8?q?[#234]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=86=8C=EC=86=8D=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Auth/Data/CollegeDepartmentStore.swift | 36 +++++ .../Presentation/Auth/View/DropDownView.swift | 11 +- .../Auth/View/SetNickNameView.swift | 18 ++- .../SetNickNameViewController.swift | 6 - .../Presentation/Map/MainMapView 2.swift | 127 ------------------ 5 files changed, 52 insertions(+), 146 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift delete mode 100644 EATSSU/App/Sources/Presentation/Map/MainMapView 2.swift diff --git a/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift b/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift new file mode 100644 index 00000000..29874c2c --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift @@ -0,0 +1,36 @@ +// +// CollegeDepartmentStore.swift +// EATSSU-DEV +// +// Created by 황상환 on 6/26/25. +// + +import Foundation + +// Sources/Data/CollegeDepartmentStore.swift + +struct CollegeDepartment { + let college: String + let departments: [String] +} + +enum CollegeDepartmentStore { + static let list: [CollegeDepartment] = [ + .init(college: "IT대학", departments: ["컴퓨터학부", "전자정보공학부\n전자공학전공", "전자정보공학부\nIT융합전공", "글로벌미디어학부", "소프트웨어학부", "AI융합학부", "미디어경영학과", "정보보호학과"]), + .init(college: "경영대학", departments: ["경영학부", "벤처중소기업학과", "회계학과", "금융학부", "벤처경영학과", "혁신경영학과", "복지경영학과", "회계세무학과"]), + .init(college: "경제통상대학", departments: ["경제학과", "글로벌통상학과", "금융경제학과", "국제무역학과"]), + .init(college: "공과대학", departments: ["화학공학과", "신소재공학과", "전기공학부", "기계공학부", "산업ㆍ정보시스템공학과", "건축학부"]), + .init(college: "법과대학", departments: ["법학과", "국제법무학과"]), + .init(college: "사회과학대학", departments: ["사회복지학부", "행정학부", "행정학부", "정보사회학과", "언론홍보학과", "평생교육학과"]), + .init(college: "인문대학", departments: ["기독교학과", "국어국문학과", "영어영문학과", "독어독문학과", "불어불문학과", "중어중문학과", "일어일문학과", "철학과", "철학과", "문예창작전공", "영화예술전공", "스포츠학부"]), + .init(college: "자연과학대학", departments: ["수학과", "물리학과", "화학과", "정보통계∙보험수리학과", "의생명시스템학부"]), + .init(college: "자유전공학부", departments: ["자유전공학부"]), + .init(college: "차세대반도체학과", departments: ["차세대반도체학과"]), + ] + + static var colleges: [String] { list.map(\.college) } + + static func departments(of college: String) -> [String] { + list.first { $0.college == college }?.departments ?? [] + } +} diff --git a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift index 59fc2cbb..5a74c499 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift @@ -16,7 +16,7 @@ final class DropDownView: UIView { // MARK: - Properties private var items: [String] - public var onSelect: ((String) -> Void)? + public var onSelectItem: ((String) -> Void)? private var isDropdownVisible = false private var dropdownTableView: UITableView? @@ -66,7 +66,6 @@ final class DropDownView: UIView { button.snp.makeConstraints { $0.edges.equalToSuperview() - $0.height.equalTo(52) } arrow.snp.makeConstraints { @@ -109,7 +108,7 @@ final class DropDownView: UIView { let origin = self.convert(self.bounds.origin, to: parentView) tableView.snp.makeConstraints { - $0.top.equalTo(origin.y + 52) + $0.top.equalTo(origin.y + 48 + 6) $0.leading.equalTo(origin.x) $0.width.equalTo(self.bounds.width) $0.height.equalTo(min(items.count * 44, 200)) @@ -162,8 +161,8 @@ extension DropDownView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selected = items[indexPath.row] - setTitle(selected) - onSelect?(selected) - toggleDropdown() + setTitle(selected) + onSelectItem?(selected) + toggleDropdown() } } diff --git a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift index 26a4ee2d..62c643ee 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift @@ -15,7 +15,7 @@ final class SetNickNameView: BaseUIView { // MARK: - Properties private var userNickname: String = "" - public let collegeDropDownView = DropDownView(title: "단과대", items: ["인문대", "자연대",]) + public let collegeDropDownView = DropDownView(title: "단과대", items: CollegeDepartmentStore.colleges) public let departmentDropDownView = DropDownView(title: "학과", items: []) // MARK: - UI Components @@ -108,6 +108,7 @@ final class SetNickNameView: BaseUIView { override init(frame: CGRect) { super.init(frame: frame) setTextFieldDelegate() + bindCollegeDepartmentDropDown() } // MARK: - Functions @@ -138,13 +139,8 @@ final class SetNickNameView: BaseUIView { } nicknameDoubleCheckButton.snp.makeConstraints { $0.top.equalTo(inputNickNameTextField) - $0.width.equalTo(75) - $0.height.equalTo(48) $0.trailing.equalToSuperview().inset(16) } - inputNickNameTextField.snp.makeConstraints { - $0.height.equalTo(48) - } affiliationLabel.snp.makeConstraints { $0.top.equalTo(setNickNameStackView.snp.bottom).offset(24) $0.leading.equalToSuperview().inset(24) @@ -175,13 +171,21 @@ final class SetNickNameView: BaseUIView { completeSettingNickNameButton.snp.makeConstraints { $0.horizontalEdges.equalToSuperview().inset(24) $0.bottom.equalTo(safeAreaLayoutGuide).inset(26) - $0.height.equalTo(52) } } func setTextFieldDelegate() { inputNickNameTextField.delegate = self } + + private func bindCollegeDepartmentDropDown() { + collegeDropDownView.onSelectItem = { [weak self] college in + guard let self else { return } + let departments = CollegeDepartmentStore.departments(of: college) + departmentDropDownView.updateItems(departments) + departmentDropDownView.setTitle("학과") // 타이틀 초기화 + } + } } // MARK: - UITextFieldDelegate diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index e4a09c59..305b004d 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -32,12 +32,6 @@ final class SetNickNameViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() - setNickNameView.collegeDropDownView.onSelect = { [weak self] selectedCollege in - // 학과 목록 업데이트 - let deptList = self?.getDepartments(for: selectedCollege) ?? [] - self?.setNickNameView.departmentDropDownView.updateItems(deptList) - } - dismissKeyboard() } diff --git a/EATSSU/App/Sources/Presentation/Map/MainMapView 2.swift b/EATSSU/App/Sources/Presentation/Map/MainMapView 2.swift deleted file mode 100644 index 5157d501..00000000 --- a/EATSSU/App/Sources/Presentation/Map/MainMapView 2.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// MainMapView 2.swift -// EATSSU -// -// Created by 황상환 on 6/24/25. -// - - -import UIKit -import NMapsMap -import SnapKit -import EATSSUDesign - -final class MainMapView: UIView { - - let mapView = NMFNaverMapView() - let titleLabel = UILabel() - let toggleBackgroundView = UIView() - let wholeButton = UIButton(type: .system) - let myOnlyButton = UIButton(type: .system) - let heartButton = UIButton(type: .system) - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupLayout() - } - - required init?(coder: NSCoder) { - fatalError() - } - - private func setupViews() { - backgroundColor = .white - - mapView.showZoomControls = false - addSubview(mapView) - - titleLabel.text = "제휴 지도" - titleLabel.textColor = .black - titleLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 20) - titleLabel.textAlignment = .center - titleLabel.backgroundColor = .white - titleLabel.layer.shadowColor = UIColor.black.cgColor - titleLabel.layer.shadowOpacity = 0.1 - titleLabel.layer.shadowOffset = CGSize(width: 0, height: 2) - titleLabel.layer.shadowRadius = 4 - addSubview(titleLabel) - - toggleBackgroundView.layer.cornerRadius = 20 - toggleBackgroundView.layer.borderWidth = 1 - toggleBackgroundView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor - toggleBackgroundView.backgroundColor = .white - addSubview(toggleBackgroundView) - - [wholeButton, myOnlyButton].forEach { - $0.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) - $0.layer.cornerRadius = 14 - $0.clipsToBounds = true - $0.backgroundColor = .clear - $0.setTitleColor(.label, for: .normal) - } - - wholeButton.setTitle("전체", for: .normal) - myOnlyButton.setTitle("내 제휴", for: .normal) - - toggleBackgroundView.addSubview(wholeButton) - toggleBackgroundView.addSubview(myOnlyButton) - - let heartImage = UIImage(systemName: "heart")? - .withConfiguration(UIImage.SymbolConfiguration(pointSize: 13, weight: .regular)) - heartButton.setImage(heartImage, for: .normal) - heartButton.tintColor = EATSSUDesignAsset.Color.Red.error.color - heartButton.backgroundColor = .white - heartButton.layer.cornerRadius = 20 - heartButton.layer.shadowColor = UIColor.black.cgColor - heartButton.layer.shadowOpacity = 0.1 - heartButton.layer.shadowRadius = 4 - heartButton.layer.shadowOffset = CGSize(width: 0, height: 2) - addSubview(heartButton) - } - - private func setupLayout() { - mapView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide.snp.top) - $0.leading.trailing.equalToSuperview() - $0.height.equalTo(60) - } - - toggleBackgroundView.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(12) - $0.centerX.equalToSuperview() - $0.width.equalTo(180) - $0.height.equalTo(40) - } - - wholeButton.snp.makeConstraints { - $0.top.bottom.equalToSuperview().inset(4) - $0.leading.equalToSuperview().inset(4) - $0.width.equalTo(80) - } - - myOnlyButton.snp.makeConstraints { - $0.top.bottom.equalToSuperview().inset(4) - $0.trailing.equalToSuperview().inset(4) - $0.width.equalTo(80) - } - - heartButton.snp.makeConstraints { - $0.trailing.equalToSuperview().inset(16) - $0.top.equalTo(titleLabel.snp.bottom).offset(12) - $0.size.equalTo(40) - } - } - - func setButtonSelection(isWholeSelected: Bool) { - wholeButton.backgroundColor = isWholeSelected ? EATSSUDesignAsset.Color.Main.primary.color : .clear - wholeButton.setTitleColor(isWholeSelected ? .white : .label, for: .normal) - - myOnlyButton.backgroundColor = isWholeSelected ? .clear : EATSSUDesignAsset.Color.Main.primary.color - myOnlyButton.setTitleColor(isWholeSelected ? .label : .white, for: .normal) - } -} From 917e5a6c9cf216e8988fc5d23a7dc1a3e6b988b0 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 30 Jun 2025 21:21:10 +0900 Subject: [PATCH 11/52] =?UTF-8?q?[#234]=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=97=AC=EB=B0=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift index 62c643ee..f3f5ec08 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift @@ -134,7 +134,7 @@ final class SetNickNameView: BaseUIView { } setNickNameStackView.snp.makeConstraints { $0.top.equalTo(nickNameLabel.snp.bottom).offset(8) - $0.leading.equalToSuperview().inset(20) + $0.leading.equalToSuperview().inset(24) $0.trailing.equalTo(nicknameDoubleCheckButton.snp.leading).offset(-5) } nicknameDoubleCheckButton.snp.makeConstraints { From 9314358bc13b9e35518e5c6c40bfd1d92e9f862e Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 30 Jun 2025 23:59:18 +0900 Subject: [PATCH 12/52] =?UTF-8?q?[#234]=20=EC=86=8C=EC=86=8D=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20API=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Router/UserNicknameRouter.swift | 8 ++++++++ .../Presentation/Auth/View/DropDownView.swift | 7 +++++++ .../SetNickNameViewController.swift | 19 +++++++++++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift b/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift index c399f36e..1ee43688 100644 --- a/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift @@ -11,6 +11,7 @@ import Moya enum UserNicknameRouter { case setNickname(nickname: String) case checkNickname(nickname: String) + case setDepartment(department: String) } extension UserNicknameRouter: TargetType { @@ -24,6 +25,8 @@ extension UserNicknameRouter: TargetType { "/users/nickname" case .checkNickname: "/users/validate/nickname" + case .setDepartment: + "/users/department" } } @@ -33,6 +36,8 @@ extension UserNicknameRouter: TargetType { .patch case .checkNickname: .get + case .setDepartment: + .post } } @@ -44,6 +49,9 @@ extension UserNicknameRouter: TargetType { case let .checkNickname(nickname: nickname): let param: [String: String] = ["nickname": nickname] return .requestParameters(parameters: param, encoding: URLEncoding.queryString) + case let .setDepartment(department: department): + let param: [String: String] = ["departmentName": department] + return .requestParameters(parameters: param, encoding: JSONEncoding.default) } } diff --git a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift index 5a74c499..f34ef0de 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift @@ -139,6 +139,13 @@ final class DropDownView: UIView { config?.titleAlignment = .leading button.configuration = config } + + public func getSelectedTitle() -> String? { + if let attributed = button.configuration?.attributedTitle { + return NSAttributedString(attributed).string + } + return nil + } } // MARK: - UITableViewDelegate, UITableViewDataSource diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index 305b004d..1fb7adaa 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -67,7 +67,11 @@ final class SetNickNameViewController: BaseViewController { @objc func tappedCompleteNickNameButton() { - setUserNickname(nickname: setNickNameView.inputNickNameTextField.text ?? "") + let nickname = setNickNameView.inputNickNameTextField.text ?? "" + let department = setNickNameView.departmentDropDownView.getSelectedTitle() ?? "" + + setUserNickname(nickname: nickname) + setUserDepartment(department: department) } @objc @@ -132,7 +136,7 @@ extension SetNickNameViewController { private func setUserNickname(nickname: String) { nicknameProvider.request(.setNickname(nickname: nickname)) { response in switch response { - case let .success(moyaResponse): + case .success(_): if let currentUserInfo = UserInfoManager.shared.getCurrentUserInfo() { UserInfoManager.shared.updateNickname(for: currentUserInfo, nickname: nickname) } @@ -192,4 +196,15 @@ extension SetNickNameViewController { } } } + + private func setUserDepartment(department: String) { + nicknameProvider.request(.setDepartment(department: department)) { response in + switch response { + case .success: + print("학과 등록 성공: \(department)") + case let .failure(error): + print("학과 등록 실패: \(error.localizedDescription)") + } + } + } } From e1e2a569993388b54b4d9900aff8178f7c864cdd Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 2 Jul 2025 00:04:41 +0900 Subject: [PATCH 13/52] =?UTF-8?q?[#234]=20=EC=A0=84=EC=B2=B4=20=EC=A0=9C?= =?UTF-8?q?=ED=9C=B4=20=EC=A7=80=EB=8F=84=201=EC=B0=A8=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EB=B0=8F=20pin=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Partnership/PartnershipDTO.swift | 22 +++++ .../Network/Router/PartnershipRouter.swift | 42 ++++++++++ .../Map/MainMapViewController.swift | 78 ++++++++++++++++++ .../cafe-pin.imageset/Contents.json | 21 +++++ .../cafe-pin.imageset/cafe-pin.png | Bin 0 -> 1207 bytes .../pub_pin.imageset/Contents.json | 21 +++++ .../pub_pin.imageset/pub_pin.png | Bin 0 -> 1562 bytes .../restaurant_pin.imageset/Contents.json | 21 +++++ .../restaurant_pin.png | Bin 0 -> 1096 bytes 9 files changed, 205 insertions(+) create mode 100644 EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift create mode 100644 EATSSU/App/Sources/Data/Network/Router/PartnershipRouter.swift create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/cafe-pin.imageset/Contents.json create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/cafe-pin.imageset/cafe-pin.png create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/pub_pin.imageset/Contents.json create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/pub_pin.imageset/pub_pin.png create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/restaurant_pin.imageset/Contents.json create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/restaurant_pin.imageset/restaurant_pin.png diff --git a/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift new file mode 100644 index 00000000..80c143fd --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift @@ -0,0 +1,22 @@ +// +// PartnershipDTO.swift +// EATSSU +// +// Created by 황상환 on 7/1/25. +// + +import Foundation + +struct PartnershipDTO: Codable { + let id: Int + let partnershipType: String + let storeName: String + let description: String + let startDate: String + let endDate: String + let restaurantType: String + let longitude: Double + let latitude: Double + let collegeNames: [String] + let departmentNames: [String] +} diff --git a/EATSSU/App/Sources/Data/Network/Router/PartnershipRouter.swift b/EATSSU/App/Sources/Data/Network/Router/PartnershipRouter.swift new file mode 100644 index 00000000..932909f1 --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/Router/PartnershipRouter.swift @@ -0,0 +1,42 @@ +// +// PartnershipRouter.swift +// EATSSU +// +// Created by 황상환 on 7/1/25. +// + +import UIKit + +import Moya + +enum PartnershipRouter { + case getAllPartnerships +} + +extension PartnershipRouter: TargetType { + var baseURL: URL { + URL(string: Config.baseURL)! + } + + var path: String { + switch self { + case .getAllPartnerships: + return "/partnerships" + } + } + + var method: Moya.Method { + return .get + } + + var task: Task { + return .requestPlain + } + + var headers: [String: String]? { + switch self { + default: + return ["Content-Type": "application/json"] + } + } +} diff --git a/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift index 44b11422..d96d2c11 100644 --- a/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift @@ -6,13 +6,18 @@ // import UIKit + import NMapsMap +import Moya import EATSSUDesign final class MainMapViewController: UIViewController { private let mainView = MainMapView() + + private let partnershipProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) + private var markers: [NMFMarker] = [] override func loadView() { self.view = mainView @@ -40,6 +45,7 @@ final class MainMapViewController: UIViewController { mainView.myOnlyButton.addTarget(self, action: #selector(didTapMyOnly), for: .touchUpInside) mainView.heartButton.addTarget(self, action: #selector(didTapHeart), for: .touchUpInside) + fetchPartnerships() } @objc private func didTapWhole() { @@ -55,4 +61,76 @@ final class MainMapViewController: UIViewController { @objc private func didTapHeart() { print("하트 버튼 클릭됨") } + + private func displayMarkers(_ partnerships: [PartnershipDTO]) { + markers.forEach { $0.mapView = nil } + markers.removeAll() + + var latSum: Double = 0 + var lngSum: Double = 0 + + for partnership in partnerships { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: partnership.latitude, lng: partnership.longitude) + + let markerImage = makeMarkerImage(title: partnership.storeName) + marker.iconImage = NMFOverlayImage(image: markerImage) + marker.width = CGFloat(UInt32(markerImage.size.width)) + marker.height = CGFloat(UInt32(markerImage.size.height)) + + marker.mapView = mainView.mapView.mapView + markers.append(marker) + + latSum += partnership.latitude + lngSum += partnership.longitude + } + + if !partnerships.isEmpty { + let centerLat = latSum / Double(partnerships.count) + let centerLng = lngSum / Double(partnerships.count) + let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: centerLat, lng: centerLng)) + mainView.mapView.mapView.moveCamera(cameraUpdate) + } + } + + private func makeMarkerImage(title: String) -> UIImage { + let label = UILabel() + label.text = "🍻 \(title)" + label.font = UIFont.systemFont(ofSize: 14, weight: .semibold) + label.textColor = .white + label.backgroundColor = UIColor(red: 72/255, green: 194/255, blue: 196/255, alpha: 1) // #48C2C4 + label.textAlignment = .center + label.layer.cornerRadius = 14 + label.layer.masksToBounds = true + label.sizeToFit() + + let padding: CGFloat = 10 + let imageSize = CGSize(width: label.frame.width + padding * 2, height: label.frame.height + padding) + UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) + label.frame = CGRect(x: padding, y: 0, width: label.frame.width, height: label.frame.height) + label.drawHierarchy(in: label.frame, afterScreenUpdates: true) + let image = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage() + UIGraphicsEndImageContext() + return image + } + + + // MARK: - Network + + private func fetchPartnerships() { + partnershipProvider.request(.getAllPartnerships) { [weak self] result in + switch result { + case .success(let response): + do { + let decoded = try response.map(BaseResponse<[PartnershipDTO]>.self) + guard let partnerships = decoded.result else { return } + self?.displayMarkers(partnerships) + } catch { + print("Decoding 실패: \(error.localizedDescription)") + } + case .failure(let error): + print("네트워크 오류: \(error.localizedDescription)") + } + } + } } diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/cafe-pin.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/cafe-pin.imageset/Contents.json new file mode 100644 index 00000000..17037b1d --- /dev/null +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/cafe-pin.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "cafe-pin.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/cafe-pin.imageset/cafe-pin.png b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/cafe-pin.imageset/cafe-pin.png new file mode 100644 index 0000000000000000000000000000000000000000..8a5f57fcda7db5493bb9e1d7b300ad4dc3633c00 GIT binary patch literal 1207 zcmV;o1W5adP)n?ml)T_m%X^8*dxF-bCIXeMB)V~{E>v7m!XE%6ZjEIn>O$PIaiLq} zhl#k-`ukUus=**yt&v`Z(m9?p<<^4j?c7=~evrRAzh(m1)g|#y-P~$tH5IANT7iJ3)@?UoH{uQJbr)T z3{=T#4#dtkwRD$iEGm$P^h^5lWpeg5@B)GHKtM)fjgi#KCZkw?D?K;rnK%YT_)FS% zxmeQ=0&~NPVel9>$dXGfW7!ZNdoXcCAY6pirJH;*W?$qj*!gHOMB;kE1%KBD;pFE3 z)zPNCZj-tf@cGqsT}`dJZePDi_iKBjQ={e&0x_FmaUsxzMfEi9fbhCH*j&3--Rr2| z3WxssN8Pi<^)xoC>utexx?fE>*VnsDAX|@o;E3)w*x~|)A8w@H!X&*|-2RPQRQu5M zQ&>Z7ERIDc7rw&q%nNWq)V87$xMV$TI!K5u?h=V%5ryJd@NTm{FYa{WE+jMaZXN;F zoT2isaul~IWq4)^o>-sYTJnv$PG-I;VSId={-o|rre?vCi*WN}?&umJy6C-6a8)7& zj>2zoY&8xX=9yK_moZ@o332cU711<5Gb73AvKFN5{F2fPjMMo6 z_~qhE=n3ZHkyDON@qh?%KTRib%GlrqY%$J#S7%>o_2&h@v9#j<<$lOk>wY|`Eskc9 z1Z&Hs8N>nj?GR)9u2T8JqKy4f<8!PDc?RJj;wa>T;fbYZbY24FKJTs`6uxY~I@&jm z-5ZaITn*;%um+k-{{^@34?FezmbOUV8adb0a+J^WlVN62_K_72gTvG}LItXo&B7k4 zmUT%k%)^y;FI|YET``~R|9)KntxLJgJ_Dim>!G6uMxtQ0ZxQYQo)*BdHv6)n~ zls<8P!rQG6B?RN=np$_QykE4Mna^YoQtt6=3gnH=H9;9E(N-Fr$o^S)SjloFxA+|E z!3)i;QFdC6ET}rjiL~2E98K^8+82&|Q{VjGoE4S|KI1%GZ$y_0rGL!dUEgqat^g|HNHI*xP2RO#Ok-vHZe V#TC^lo)iE8002ovPDHLkV1lVMKHdNT literal 0 HcmV?d00001 diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/pub_pin.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/pub_pin.imageset/Contents.json new file mode 100644 index 00000000..b0fa9542 --- /dev/null +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/pub_pin.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pub_pin.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/pub_pin.imageset/pub_pin.png b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/pub_pin.imageset/pub_pin.png new file mode 100644 index 0000000000000000000000000000000000000000..3969664d46f61838812f09680c7d5911d0fb8aba GIT binary patch literal 1562 zcmV+#2IcvQP)Dy8rXB0t6?(Yg>aEJ%cgu7&P@?3k^=|hePDq_)%E4ZXX08e_;DN$R37k|AsU>I`b02;GHSPB}w z{$9t;d_s8%e)(u_j1Ea_c`*K6W5-agK}&)#=~i^(7CUAVL8OSj4gnBFaCIw6 zvBVy&Y;V$1>{arf`?YLUxi3X9^iPDQo-HZ)Jm8$`1{W3CN`t2M9+M5e;0e$BPw_-+ z>fY=2P*qulYSXd09iArR~9p0#u|dAs>*!S;61L0e0Ansy0mmZLvHMyc53GZIw2U_bRqwas3>-lQmT1! zm(;6?r27Yk!7-V!gsTu0M$(L`j zgCj_EplIlT0r3WNth&5{K7XS&B@^KV!5v_Yd9sJW<(!9Wwif9?xSE3j(0&Dft7NTK z9r39N)it-8ILIJsuZDkCejADJDD%P(p-m?cB)dFPh+xtf`SDum5!OyLb+1gzzYs+- zO=dVyP1b3i$PWaV*?*!v#~{=~@xcV0YEfzosGjM`K2O9u)8$b9zZ+p4HY2hz^e z^CidR04XUX)K(|tG!=-;wQZ`Bfku#oADY?(U{2&ZBypG>K6f>|K!w63MYqfR;&<$| z=|JMX^YTg3=hF6Ky^CNBiP=f?XM=ZY*up6SaK1YN^6k8V*ch#km~~qz9-y9_vE?m za0Wquze`bOk5K{8#M6sbkS=$-^AgDgm!Q7MwDX#Mm8zYo|4Xp;CayJh4rcaQ;NqjX z;vRh{A~M9}+OqpCtC^j6;#1B&Au1AZuvzXix^s->MlsB7W!7}$tp~3+bq>O55m|KT zkl4M3@{t2<3syuF!k!E{MTZ1v&FeF2(hrGp%NBtC#p$3CcZ~=aPWDBR3Ic0P(uB9d z7`Hm8q{ASXj^qcw`lfE-6Vf25gD2rnkwGDBh!D54T$O|l`frGT0e#_<=$)&S=Kufz M07*qoM6N<$g8w$@!~g&Q literal 0 HcmV?d00001 diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/restaurant_pin.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/restaurant_pin.imageset/Contents.json new file mode 100644 index 00000000..00667e4b --- /dev/null +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/restaurant_pin.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "restaurant_pin.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/restaurant_pin.imageset/restaurant_pin.png b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/restaurant_pin.imageset/restaurant_pin.png new file mode 100644 index 0000000000000000000000000000000000000000..09a4c23fab8e0c6ccc35f7410129aa142a6406d4 GIT binary patch literal 1096 zcmV-O1h@N%P)dM39vcfhM}cg$wt%;IX5uTmqkfva7HVk9D{ydYWtej@mVw36;SmhRr*C%6Xf@IlIIyui0a>z| z2Ck`=u8~xgNd?l727JeJIN5u^447E6TDim3=251S{4&|BRA^+i^g`m!Bw-&s)$H%cpIJ+;$`*IJ@B#^z&uh=m#& z)e=WES<9pdqN1QsmWvdNEYp1|$J^oq^wu=Udk<@_L&v2m&$f6f!*o)}D}gj~ zFI$imJ5Z!fY@|)$z;j`AvaT-OA%-%#|g1Kr=Tdh`Q~Ko<0Z ziR=W9jWqWvxKvCNNYjHxK|jKyFb->A0<<{he||O82VJdCxXr*4n853UJ%iRm`w3W6 zP+`t3_y&DJ1O8wf3iy?1#GkWyxEJ)pJX+5v_!jYywur+j#3ba1E!?~fZq8BIB7I7M zzr)-G{~64pj?-BRoN0G>MGmcK6iSV_n;jmyj>jaSFdqX0b1|r@^7VEYPQEe&oNLyn z7Kd4}sP!V{1TZcoH+upu5cL0^xD)aQixc4 z*3#X6{(jQj%xOq{gnN%EuRwwlo5dyEk)kd&{p(9E7LBfQ1q~}&E@g^O(ds Date: Wed, 2 Jul 2025 02:03:39 +0900 Subject: [PATCH 14/52] =?UTF-8?q?[#234]=20=EC=A0=84=EC=B2=B4=20=EC=A0=9C?= =?UTF-8?q?=ED=9C=B4=20=EC=A7=80=EB=8F=84=20=EB=A7=88=EC=BB=A4=20UI=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/{ => View}/MainMapView.swift | 0 .../Presentation/Map/View/MapMarkerView.swift | 60 +++++++++++++++++++ .../MainMapViewController.swift | 50 +++++++++------- 3 files changed, 88 insertions(+), 22 deletions(-) rename EATSSU/App/Sources/Presentation/Map/{ => View}/MainMapView.swift (100%) create mode 100644 EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift rename EATSSU/App/Sources/Presentation/Map/{ => ViewController}/MainMapViewController.swift (77%) diff --git a/EATSSU/App/Sources/Presentation/Map/MainMapView.swift b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift similarity index 100% rename from EATSSU/App/Sources/Presentation/Map/MainMapView.swift rename to EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift diff --git a/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift new file mode 100644 index 00000000..f7a3763c --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift @@ -0,0 +1,60 @@ +// +// MapMarkerView.swift +// EATSSU +// +// Created by 황상환 on 7/2/25. +// + +import UIKit + +import EATSSUDesign + +final class MapMarkerView: UIView { + + private let iconImageView = UIImageView() + private let titleLabel = UILabel() + + init(icon: UIImage?, title: String) { + super.init(frame: .zero) + setupUI(icon: icon, title: title) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI(icon: UIImage?, title: String) { + backgroundColor = .white + layer.cornerRadius = 13 + layer.masksToBounds = true + layer.borderWidth = 1 + layer.borderColor = UIColor.systemGray3.cgColor + + iconImageView.image = icon + iconImageView.contentMode = .scaleAspectFit + iconImageView.snp.makeConstraints { $0.size.equalTo(20) } + + titleLabel.text = title + titleLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 10) + titleLabel.textColor = .black + + let hStack = UIStackView(arrangedSubviews: [iconImageView, titleLabel]) + hStack.axis = .horizontal + hStack.alignment = .center + hStack.spacing = 2 + hStack.layoutMargins = UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 8) + hStack.isLayoutMarginsRelativeArrangement = true + + addSubview(hStack) + hStack.snp.makeConstraints { $0.edges.equalToSuperview() } + } + + func toImage() -> UIImage { + layoutIfNeeded() + let renderer = UIGraphicsImageRenderer(size: bounds.size) + return renderer.image { ctx in + layer.render(in: ctx.cgContext) + } + } + +} diff --git a/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift similarity index 77% rename from EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift rename to EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index d96d2c11..c02e9668 100644 --- a/EATSSU/App/Sources/Presentation/Map/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -73,7 +73,7 @@ final class MainMapViewController: UIViewController { let marker = NMFMarker() marker.position = NMGLatLng(lat: partnership.latitude, lng: partnership.longitude) - let markerImage = makeMarkerImage(title: partnership.storeName) + let markerImage = makeMarkerImage(type: partnership.restaurantType, title: partnership.storeName) marker.iconImage = NMFOverlayImage(image: markerImage) marker.width = CGFloat(UInt32(markerImage.size.width)) marker.height = CGFloat(UInt32(markerImage.size.height)) @@ -92,29 +92,35 @@ final class MainMapViewController: UIViewController { mainView.mapView.mapView.moveCamera(cameraUpdate) } } - - private func makeMarkerImage(title: String) -> UIImage { - let label = UILabel() - label.text = "🍻 \(title)" - label.font = UIFont.systemFont(ofSize: 14, weight: .semibold) - label.textColor = .white - label.backgroundColor = UIColor(red: 72/255, green: 194/255, blue: 196/255, alpha: 1) // #48C2C4 - label.textAlignment = .center - label.layer.cornerRadius = 14 - label.layer.masksToBounds = true - label.sizeToFit() - - let padding: CGFloat = 10 - let imageSize = CGSize(width: label.frame.width + padding * 2, height: label.frame.height + padding) - UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) - label.frame = CGRect(x: padding, y: 0, width: label.frame.width, height: label.frame.height) - label.drawHierarchy(in: label.frame, afterScreenUpdates: true) - let image = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage() - UIGraphicsEndImageContext() - return image + + private func makeMarkerImage(type: String, title: String) -> UIImage { + let icon: UIImage? + + switch type { + case "RESTAURANT": + icon = EATSSUDesignAsset.Images.restaurantPin.image + case "CAFE": + icon = EATSSUDesignAsset.Images.cafePin.image + case "PUB": + icon = EATSSUDesignAsset.Images.pubPin.image + default: + icon = EATSSUDesignAsset.Images.restaurantPin.image + } + + let markerView = MapMarkerView(icon: icon, title: title) + markerView.setNeedsLayout() + markerView.layoutIfNeeded() + + let fittingSize = markerView.systemLayoutSizeFitting( + CGSize(width: UIView.layoutFittingCompressedSize.width, + height: 24) + ) + + markerView.frame = CGRect(origin: .zero, size: fittingSize) + return markerView.toImage() + } - // MARK: - Network private func fetchPartnerships() { From c11e1f3f9525ace68c6a2a51ac96f8fc9a1da993 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:53:24 +0900 Subject: [PATCH 15/52] =?UTF-8?q?[#234]=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20?= =?UTF-8?q?=EB=94=94=ED=85=8C=EC=9D=BC=20=EB=B7=B0=201=EC=B0=A8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Map/View/MapMarkerView.swift | 2 +- .../MainMapViewController.swift | 11 ++- ...PartnershipDetailSheetViewController.swift | 94 +++++++++++++++++++ 3 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift diff --git a/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift index f7a3763c..6fefc682 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift @@ -32,7 +32,7 @@ final class MapMarkerView: UIView { iconImageView.image = icon iconImageView.contentMode = .scaleAspectFit - iconImageView.snp.makeConstraints { $0.size.equalTo(20) } + iconImageView.snp.makeConstraints { $0.width.equalTo(20) } titleLabel.text = title titleLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 10) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index c02e9668..2def52ce 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -78,6 +78,12 @@ final class MainMapViewController: UIViewController { marker.width = CGFloat(UInt32(markerImage.size.width)) marker.height = CGFloat(UInt32(markerImage.size.height)) + marker.touchHandler = { [weak self] _ in + let vc = PartnershipDetailSheetViewController(partnership: partnership) + self?.present(vc, animated: true) + return true + } + marker.mapView = mainView.mapView.mapView markers.append(marker) @@ -112,8 +118,9 @@ final class MainMapViewController: UIViewController { markerView.layoutIfNeeded() let fittingSize = markerView.systemLayoutSizeFitting( - CGSize(width: UIView.layoutFittingCompressedSize.width, - height: 24) + CGSize(width: UIView.layoutFittingCompressedSize.width, height: 24), + withHorizontalFittingPriority: .fittingSizeLevel, + verticalFittingPriority: .required ) markerView.frame = CGRect(origin: .zero, size: fittingSize) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift new file mode 100644 index 00000000..2c9bb59b --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -0,0 +1,94 @@ +// +// PartnershipDetailSheetViewController.swift +// EATSSU +// +// Created by 황상환 on 7/2/25. +// + +import UIKit + +import EATSSUDesign + +final class PartnershipDetailSheetViewController: UIViewController { + + private let partnership: PartnershipDTO + + init(partnership: PartnershipDTO) { + self.partnership = partnership + super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .pageSheet + if let sheet = sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + } + } + + required init?(coder: NSCoder) { + fatalError() + } + + private let storeNameLabel = UILabel() + private let descriptionLabel = UILabel() + private let dateLabel = UILabel() + private let collegeLabel = UILabel() + private let departmentLabel = UILabel() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + setupViews() + setupLayout() + configureData() + } + + private func setupViews() { + storeNameLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 18) + descriptionLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + dateLabel.font = EATSSUDesignFontFamily.Pretendard.light.font(size: 12) + collegeLabel.font = EATSSUDesignFontFamily.Pretendard.light.font(size: 12) + departmentLabel.font = EATSSUDesignFontFamily.Pretendard.light.font(size: 12) + + descriptionLabel.numberOfLines = 0 + collegeLabel.numberOfLines = 0 + departmentLabel.numberOfLines = 0 + + [storeNameLabel, descriptionLabel, dateLabel, collegeLabel, departmentLabel].forEach { + view.addSubview($0) + } + } + + private func setupLayout() { + storeNameLabel.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide).offset(24) + $0.leading.trailing.equalToSuperview().inset(20) + } + + descriptionLabel.snp.makeConstraints { + $0.top.equalTo(storeNameLabel.snp.bottom).offset(16) + $0.leading.trailing.equalToSuperview().inset(20) + } + + dateLabel.snp.makeConstraints { + $0.top.equalTo(descriptionLabel.snp.bottom).offset(16) + $0.leading.trailing.equalToSuperview().inset(20) + } + + collegeLabel.snp.makeConstraints { + $0.top.equalTo(dateLabel.snp.bottom).offset(16) + $0.leading.trailing.equalToSuperview().inset(20) + } + + departmentLabel.snp.makeConstraints { + $0.top.equalTo(collegeLabel.snp.bottom).offset(8) + $0.leading.trailing.equalToSuperview().inset(20) + } + } + + private func configureData() { + storeNameLabel.text = partnership.storeName + descriptionLabel.text = partnership.description + dateLabel.text = "기간: \(partnership.startDate) ~ \(partnership.endDate)" + collegeLabel.text = "대학: \(partnership.collegeNames.joined(separator: ", "))" + departmentLabel.text = "학과: \(partnership.departmentNames.joined(separator: ", "))" + } +} From b0f9f97cd01b5943e9219ff97c3672f149d8642a Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:39:52 +0900 Subject: [PATCH 16/52] =?UTF-8?q?[#234]=20=EC=A0=9C=ED=9C=B4=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20API=EC=97=90=20=EB=A7=9E=EC=B6=B0=EC=84=9C=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 --- .../DTO/Partnership/PartnershipDTO.swift | 18 ++++++++++----- .../Network/Router/UserNicknameRouter.swift | 1 + .../Presentation/Map/View/MapMarkerView.swift | 2 +- .../MainMapViewController.swift | 12 +++++++--- ...PartnershipDetailSheetViewController.swift | 23 +++++++++++-------- 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift index 80c143fd..6b65069c 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift @@ -8,15 +8,21 @@ import Foundation struct PartnershipDTO: Codable { + let storeName: String + let longitude: Double + let latitude: Double + let restaurantType: String + let partnershipInfos: [PartnershipInfoDTO] +} + +struct PartnershipInfoDTO: Codable { let id: Int let partnershipType: String - let storeName: String + let collegeName: String? + let departmentName: String + let likeCount: Int + let isLiked: Bool let description: String let startDate: String let endDate: String - let restaurantType: String - let longitude: Double - let latitude: Double - let collegeNames: [String] - let departmentNames: [String] } diff --git a/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift b/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift index 1ee43688..b09ff061 100644 --- a/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift @@ -12,6 +12,7 @@ enum UserNicknameRouter { case setNickname(nickname: String) case checkNickname(nickname: String) case setDepartment(department: String) +// case check } extension UserNicknameRouter: TargetType { diff --git a/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift index 6fefc682..fdd112a0 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift @@ -42,7 +42,7 @@ final class MapMarkerView: UIView { hStack.axis = .horizontal hStack.alignment = .center hStack.spacing = 2 - hStack.layoutMargins = UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 8) + hStack.layoutMargins = UIEdgeInsets(top: 3, left: 2, bottom: 3, right: 8) hStack.isLayoutMarginsRelativeArrangement = true addSubview(hStack) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 2def52ce..1058d212 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -79,9 +79,15 @@ final class MainMapViewController: UIViewController { marker.height = CGFloat(UInt32(markerImage.size.height)) marker.touchHandler = { [weak self] _ in - let vc = PartnershipDetailSheetViewController(partnership: partnership) - self?.present(vc, animated: true) - return true + guard let info = partnership.partnershipInfos.first else { return true } + + let vc = PartnershipDetailSheetViewController( + storeName: partnership.storeName, + restaurantType: partnership.restaurantType, + info: info + ) + self?.present(vc, animated: true) + return true } marker.mapView = mainView.mapView.mapView diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 2c9bb59b..def06c82 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -11,10 +11,15 @@ import EATSSUDesign final class PartnershipDetailSheetViewController: UIViewController { - private let partnership: PartnershipDTO - - init(partnership: PartnershipDTO) { - self.partnership = partnership + private let storeName: String + private let restaurantType: String + private let info: PartnershipInfoDTO + + init(storeName: String, restaurantType: String, info: PartnershipInfoDTO) { + self.storeName = storeName + self.restaurantType = restaurantType + self.info = info + super.init(nibName: nil, bundle: nil) modalPresentationStyle = .pageSheet if let sheet = sheetPresentationController { @@ -85,10 +90,10 @@ final class PartnershipDetailSheetViewController: UIViewController { } private func configureData() { - storeNameLabel.text = partnership.storeName - descriptionLabel.text = partnership.description - dateLabel.text = "기간: \(partnership.startDate) ~ \(partnership.endDate)" - collegeLabel.text = "대학: \(partnership.collegeNames.joined(separator: ", "))" - departmentLabel.text = "학과: \(partnership.departmentNames.joined(separator: ", "))" + storeNameLabel.text = storeName + descriptionLabel.text = info.description + dateLabel.text = "기간: \(info.startDate) ~ \(info.endDate)" + collegeLabel.text = "대학: \(info.collegeName ?? "-")" + departmentLabel.text = "학과: \(info.departmentName)" } } From ffdfe5bc9c95f27d72447bd6d7e5394d622a8c52 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Fri, 4 Jul 2025 21:14:05 +0900 Subject: [PATCH 17/52] =?UTF-8?q?[#234]=20=EC=A0=9C=ED=9C=B4=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EB=94=94=ED=85=8C=EC=9D=BC=20=EB=B7=B0=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=88=98=EC=A0=95=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainMapViewController.swift | 6 +- ...PartnershipDetailSheetViewController.swift | 150 +++++++++++++----- 2 files changed, 114 insertions(+), 42 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 1058d212..3bc65234 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -79,16 +79,14 @@ final class MainMapViewController: UIViewController { marker.height = CGFloat(UInt32(markerImage.size.height)) marker.touchHandler = { [weak self] _ in - guard let info = partnership.partnershipInfos.first else { return true } - let vc = PartnershipDetailSheetViewController( storeName: partnership.storeName, restaurantType: partnership.restaurantType, - info: info + partnershipInfos: partnership.partnershipInfos ) self?.present(vc, animated: true) return true - } + } marker.mapView = mainView.mapView.mapView markers.append(marker) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index def06c82..1630d522 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -6,21 +6,21 @@ // import UIKit - +import SnapKit import EATSSUDesign final class PartnershipDetailSheetViewController: UIViewController { private let storeName: String private let restaurantType: String - private let info: PartnershipInfoDTO - - init(storeName: String, restaurantType: String, info: PartnershipInfoDTO) { + private let partnershipInfos: [PartnershipInfoDTO] + + init(storeName: String, restaurantType: String, partnershipInfos: [PartnershipInfoDTO]) { self.storeName = storeName self.restaurantType = restaurantType - self.info = info - + self.partnershipInfos = partnershipInfos super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .pageSheet if let sheet = sheetPresentationController { sheet.detents = [.medium(), .large()] @@ -33,10 +33,10 @@ final class PartnershipDetailSheetViewController: UIViewController { } private let storeNameLabel = UILabel() - private let descriptionLabel = UILabel() - private let dateLabel = UILabel() - private let collegeLabel = UILabel() - private let departmentLabel = UILabel() + private let typeStackView = UIStackView() + private let typeIconImageView = UIImageView() + private let typeTextLabel = UILabel() + private let infoListStackView = UIStackView() override func viewDidLoad() { super.viewDidLoad() @@ -47,53 +47,127 @@ final class PartnershipDetailSheetViewController: UIViewController { } private func setupViews() { - storeNameLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 18) - descriptionLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) - dateLabel.font = EATSSUDesignFontFamily.Pretendard.light.font(size: 12) - collegeLabel.font = EATSSUDesignFontFamily.Pretendard.light.font(size: 12) - departmentLabel.font = EATSSUDesignFontFamily.Pretendard.light.font(size: 12) + storeNameLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 16) + storeNameLabel.textColor = .label - descriptionLabel.numberOfLines = 0 - collegeLabel.numberOfLines = 0 - departmentLabel.numberOfLines = 0 + typeIconImageView.contentMode = .scaleAspectFit + typeIconImageView.snp.makeConstraints { $0.width.height.equalTo(18) } + + typeTextLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 10) + typeTextLabel.textColor = .gray + + typeStackView.axis = .horizontal + typeStackView.alignment = .center + typeStackView.spacing = 4 + typeStackView.addArrangedSubview(typeIconImageView) + typeStackView.addArrangedSubview(typeTextLabel) + + infoListStackView.axis = .vertical + infoListStackView.spacing = 0 + infoListStackView.alignment = .fill + infoListStackView.distribution = .fill + infoListStackView.isLayoutMarginsRelativeArrangement = true + infoListStackView.layoutMargins = .init(top: 10, left: 0, bottom: 10, right: 0) - [storeNameLabel, descriptionLabel, dateLabel, collegeLabel, departmentLabel].forEach { + [storeNameLabel, typeStackView, infoListStackView].forEach { view.addSubview($0) } } private func setupLayout() { storeNameLabel.snp.makeConstraints { - $0.top.equalTo(view.safeAreaLayoutGuide).offset(24) - $0.leading.trailing.equalToSuperview().inset(20) + $0.top.equalTo(view.safeAreaLayoutGuide).offset(20) + $0.leading.trailing.equalToSuperview().inset(24) } - descriptionLabel.snp.makeConstraints { - $0.top.equalTo(storeNameLabel.snp.bottom).offset(16) - $0.leading.trailing.equalToSuperview().inset(20) + typeStackView.snp.makeConstraints { + $0.top.equalTo(storeNameLabel.snp.bottom).offset(4) + $0.leading.equalTo(storeNameLabel) } - dateLabel.snp.makeConstraints { - $0.top.equalTo(descriptionLabel.snp.bottom).offset(16) - $0.leading.trailing.equalToSuperview().inset(20) + infoListStackView.snp.makeConstraints { + $0.top.equalTo(typeStackView.snp.bottom).offset(10) + $0.leading.trailing.equalToSuperview().inset(24) } + } + + private func configureData() { + storeNameLabel.text = storeName - collegeLabel.snp.makeConstraints { - $0.top.equalTo(dateLabel.snp.bottom).offset(16) - $0.leading.trailing.equalToSuperview().inset(20) + switch restaurantType { + case "RESTAURANT": + typeIconImageView.image = EATSSUDesignAsset.Images.restaurantPin.image + typeTextLabel.text = "음식점" + case "CAFE": + typeIconImageView.image = EATSSUDesignAsset.Images.cafePin.image + typeTextLabel.text = "카페" + case "PUB": + typeIconImageView.image = EATSSUDesignAsset.Images.pubPin.image + typeTextLabel.text = "주점" + default: + typeIconImageView.image = EATSSUDesignAsset.Images.restaurantPin.image + typeTextLabel.text = restaurantType } - departmentLabel.snp.makeConstraints { - $0.top.equalTo(collegeLabel.snp.bottom).offset(8) - $0.leading.trailing.equalToSuperview().inset(20) + for (index, info) in partnershipInfos.enumerated() { + let isLast = index == partnershipInfos.count - 1 + let card = makeInfoCard(info: info, isLast: isLast) + infoListStackView.addArrangedSubview(card) } } - private func configureData() { - storeNameLabel.text = storeName + private func makeInfoCard(info: PartnershipInfoDTO, isLast: Bool) -> UIView { + let collegeName = info.collegeName ?? info.departmentName + let start = String(info.startDate.dropFirst(5)) + let end = String(info.endDate.dropFirst(5)) + + let titleDateLabel = UILabel() + let fullText = "\(collegeName) \(start) ~ \(end)" + let attrText = NSMutableAttributedString(string: fullText) + + let collegeRange = (fullText as NSString).range(of: collegeName) + let dateRange = (fullText as NSString).range(of: "\(start) ~ \(end)") + + attrText.addAttributes([ + .font: EATSSUDesignFontFamily.Pretendard.medium.font(size: 14), + .foregroundColor: UIColor.label + ], range: collegeRange) + + attrText.addAttributes([ + .font: EATSSUDesignFontFamily.Pretendard.regular.font(size: 10), + .foregroundColor: EATSSUDesignColors.Color.gray700, + .baselineOffset: +2 + ], range: dateRange) + + titleDateLabel.attributedText = attrText + + let descriptionLabel = UILabel() + descriptionLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 12) + descriptionLabel.textColor = EATSSUDesignColors.Color.gray700 + descriptionLabel.numberOfLines = 0 descriptionLabel.text = info.description - dateLabel.text = "기간: \(info.startDate) ~ \(info.endDate)" - collegeLabel.text = "대학: \(info.collegeName ?? "-")" - departmentLabel.text = "학과: \(info.departmentName)" + + let contentStack = UIStackView(arrangedSubviews: [titleDateLabel, descriptionLabel]) + contentStack.axis = .vertical + contentStack.spacing = 4 + + let separator = UIView() + separator.backgroundColor = EATSSUDesignColors.Color.gray200 + separator.isHidden = isLast + separator.snp.makeConstraints { + $0.height.equalTo(1) + } + + let container = UIStackView(arrangedSubviews: [contentStack, separator]) + container.axis = .vertical + container.spacing = 10 + + let paddedContainer = UIView() + paddedContainer.addSubview(container) + container.snp.makeConstraints { + $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0)) + } + + return paddedContainer } } From 86d3a3ea799a6a1db8c19e498165130550757139 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sun, 6 Jul 2025 23:47:14 +0900 Subject: [PATCH 18/52] =?UTF-8?q?[#234]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=95=99=EA=B3=BC=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EC=8B=9C=20=EB=B2=84=ED=8A=BC=20=ED=99=9C=EC=84=B1=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AC=B8=EA=B5=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EATSSU/App/Sources.zip | Bin 0 -> 253001 bytes .../Auth/View/SetNickNameView.swift | 30 ++++++- .../SetNickNameViewController.swift | 75 +++++++++++------- 3 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 EATSSU/App/Sources.zip diff --git a/EATSSU/App/Sources.zip b/EATSSU/App/Sources.zip new file mode 100644 index 0000000000000000000000000000000000000000..2cf20715e6a4840ab8c1aa57bea5680ad4337138 GIT binary patch literal 253001 zcmd3P1yt3`_CMW7NQ0DgNjFG$cXxNEfYROFA>CaPf`HN>jR>d+h!O(Q@aK5%c`6sV z-uK?$_j9fBd(K*{y}$FB&)&0V&&)n@lHd?%Ab@{{SyLKcKm7S036j{5Rk8btgM6x0)4!!YIbtj08hEEQeZ$}{vrkSQi`swG{2y%qS`$=-9H81 zBo8#&)XY#1-OyCeKyO=GN@W{KC0OnuSTq(mIWjJ&JrT0sVHkEWSR@eqB!?siC1oHflb3yTxBN3znLy+upK--WdhSqIcy@!&9(TI0>0W#}ZPTOQ#J05M z2y1(Y7`Xm0`#v&d))(-p)wiq!)!t6g8)_JP;rk@^77gOmiA)UU;}#RrADB1dUeuhM zI9hr(9?){@pPGJ|@n72QpgZIkI5~QKrzX1AP;8pq4U(&~XCEX|2(N(SQB#*g`@M_S ztq79hS>?F7@{&Dk&7oBB4a7pd(~tLhN46N*6@w4x{XvC@lW8W>1!zpbnD4}?;F=R@ zpbtE*S6RWoy`y(TKR;YZS5=xA^OZ^#_HvWw_GFf00= zkt)%$_44Ka@y(r72~k;;MUJv0jB^MLlh!u~$@fBP+g|`dDfGj6oN>UL*{b*uqJs<9 z#;gl;xSrdAW*Ihxh)k<~i_hBD1p^RB z=fQ;X8-N*L-9#X~?@14|Av)b(;?m9Zug1NkKRVwah~hF~=RFUN*q<8x~hZVP1~~ zK*t|A4{wypVG+b~Lte`Z+J&dl{Gk+czw(iu?&LzL$O-CP2ZzACs4E z8b4ry{2ecpoy@Jwo!qabhHYiL>DFblxrg~3HLgm-xtk$KII4}RNJDK_6}EkV$>k2rc^?U*0W#)lbjjw{@qE;r+~#ZyTG`{-!@Y?K z1iH3HSP7Ca3i?JE-4gh~oFu(BVV0_$qsw_#n*v|nkj9i2OdN&s(;mL6a_(`&+Z;^x z7XeQ#jxQ50;GlqSN&Y2gZ-FDqNpvtPZ%2-AZVcAC-J2~8X zGHEaBPnmNsigRdkTPj~LIc#uQwe=Xba5+zXXxdj?eHX9HW!3m4(^Rd_8C$AZ3QhCw zBC3OC42EO#jsO}q(Vgr8g2*f>CwL+Pzm(E5meg&bTr0)UQshApIRY>}AJ$h1v{UBb zrJJ=bJe%`;&@C$*_hu5A3uBUfbn%?=)PfU(rm&>qw#qA&2L;BT6%`kWl{8?bGf-{o z$uyF#`$&M17z-Xyk9qCBd0{b1vdDWx{RJtw_)99x%UwR^Q zi1|q32=#hcjwwz^gm>hqpv`8Z^bkcJ_cWrgPMhz_cjA-XhH|NjgkD|OinNAVPZrkF zq`_rfRi<;z9Dh?=JjxtsMP^|%;OS0 zEU6a*jz&b{O=D>SRcZL*YKosTq@_ACaz&=kyogLXvviss7-hhwq$6RbAh$he3T~Eo zgUsBqyB$zF9SVD9Pb?8i46**E6I6iGL1r=z8>21m=(8s*gAbKb4>kiTJgg3vwJk?f z)RUaBypvf6d>gpTyYfs%;)+|SD~t@rB`WM<>oHDUX9^bEX&oBJi*&GG`aoa9gWrR- zH5rJdyZvcV=Hv3qWoymT4#m&bHc_C=IYzIW1j(w^t8t;_=Fl*dN*}Dy2wM%}w)vaB z?4}Mx@4{bj_3;L2(|~AXandzlNYyd6V?F=ET{bTgW?lJkeyu8Lh~F6w>yETj?5O)B zvwMb7`rTkyy4`J9RN0cH9KI$ zMRo@>Xn@hub=zapCDJgXkQ{OGsC6CT?yBPPz zAtNYzf(AEZV+o;rfD8RRps@(&-ebVT7pfDV$pSqVj)1^)y0wupx@!UTHTEa`Ru?_8kZA zL$#9T*`l4@%F)SveU=M&B5$m)x6XLmw;%>?Rl00^2;7(C+s+Cow6V&eUU=0415=GI z3*d$YNU(LyB_cmA7_Op1bR9STfam5_0{|Sr_}|Qde{0pioj&(NB=miB`1RFfjGbI< z9W1X6htP<%&4BG28Lo=FHah&X+kZ;GE`|UKzPZ!?KUNJ_ZTc^mNZA_dTL}qViwSVC z^KY!A{>0=@>DOTb6#l=*AAmx5gM9$S|KCF8y2u}x3HW~Xb5G3H z`g&x5k^jvr3XPfVm|TmFqMen#quEcH17Q8l=0x@j`23;d zI)s4o|Clqp7UAW$x6*#ej{W^25`@7DlT+R_cW+i2#W`FR%_&>ww#u=2gMbixb z(isDupOR%IyXnz7bz44g29El5MS@m>uf88$&Yy5cdlrgcFqoLAUN)r06&B7vavX3< z;*CGQg4lPC$8Fto;I}O)9@j4SToMdq5U-^VgcHP!aOiVCDl-i{1(Qh}gU1qw_=GFi zG7W*{+vkRh0n~0mmQ|~7E*y>@5%kp0u_?lR7`9=^(|S;sDQwW=!W}NEX7*}$?I?16%RYVYE`!L8H&)DK&%xsa#Qu@Sx4omf@ zNV!t^NW7F6KdH{IJ8w5_Q*s+86bYNtkUglanZs*%WPJ$7{}GHQ{gEsLKk~Cgzh*w_ zN;U*vLe}U9UQ1@+84^rZs-qvI2<{AxL2)LS9A^5ejNxdAI@Uds$+sUfa68mHTv=ZZ zST~DgJ!dxdoxy=q@W{NlrwDNoUvFJ6t>(oMzp9a$P-qU_X>}_s4MbQ6LuIKk zBM;GuN*Wi`R}J$%VE%u{0`6=FdUgK4I<}O*z|vnD=9`K7Z^Z#uQ#ybg#y>ktC!`27f7U6Wva#~xtDTFTtX*g@awT6BOh&dqTY@pa?@il@{4Q^9q}0OkMh zk@BW1o-*_H!b@A-mI?84gqD;=>nu*hY3*d{~nX?=PKZPD~k)-THD#$ zT<)#@2j2MFxeBO)PWL+{|F;l&)%xSE`}dK~4>?64CE07?A=)uC1GaBI=c?Fiea;W^ z>2xnOT!#%%`0r&8V88Wha`gN4{*Ui|(~%@VZWOTI2RuI|x59PNV+D+4xxh3TISsx6 zKZMGen9bpR=(j|S4w13Drr{8n>ta{3c0~Aq=tb5*EG~#;MGy-zh#Q(=fa=E>s=66{ z&kee>^;ZPya82^KxW{fKm;x|F&kmj{&fR;T5s3T-9{Wp>7&#{q4sr zhFqf4b%o$xKig>Z_66A+?K)lN)p<2)kbMM|(E@jU{0_E!h@RORI?vZ57Ib_H{Cq}{ z#oz^Nw<2iI5gOr?>sU<(CNS7@2e5<|Zi}+-{!0q|DqPntp?}2f=2SWmxtmA+-&#Un zb(Y^dh96#0(ALJp-1J)J1Kir*pToJ?M^2+4pRwpUzH*#BjQANrPo55)`jd< z1t}XyMR2_w8o1uF*<{mh%dK@#pN6tMMnUlTGCzTM9`CXGux)GwdK}8DIf%R$XA{Y( zT)a!J>2F~!-$3cr;oBJ8%H&(c=Bv9S=_hk1?9r(i(y*|~==NI^@@YtgbXSPJdkPLw zh?~oxjlD%oRHzcJ+U6QxauG3!7ftP_;Ey^uK4tBJs|4`ONAC9K$jk3}R@8rNmj2wf z7bSB5>jCTG>^)=2y@Zh=u70}VVI{vs*vq0@X$KZCDd|5Thn>*wvh66jzLnnFFqB6o z@qp_A$~scXX4g(#M*Ie|Cb(4peG@16cbwhx?GX>ya-_e&1LmC!`a;rmP!N#!kl$}; zUu75LPkjlHW^}s$jA*~#Vy^l}K>YNB_XT!E+0ocR+{VQA=hgz4LYj}XzBXrt0hum7 zw1op|n-&Yq1_-tSSpHl(m=G)~n<4tP=opu|LGW?pFXs=rlDr>kcEH~6GB?kmwO2WC z9&V1>@ftV)eUI9cd{0rz5~2RgUQ&by}|$9~{>2460{ukfv)t*FJ3%I*=a$9TtW z=k<%NJDy&Z6!mJc`2tT7#MA52t4JEnq#kpNL(ywLAyTfY?R+v%jutDP_twalROsF9 zL2x;NJa}BhqZqBq6iTr+51E*|S% zTg{wxAI{Pj&eT}RA<8uxMof>giuETtcwYo6=_mqA2g#@%GMG47~B} z#zzlakg0f)`dHXO0n4SQqw)Of?ozz0?=#;^8f$=mPvfgY4mjQSuXFm>j{W7b;+IGN zW(xl|kEFPqhra-!lt8(k3zCbCjb(DUIL(!_I*IVL>r8D+X@5PEa<%J#XyDo&1N-|+ zCvh4sNe^fdu^qNA`Ox}YFVJOEtYEDkso}uNDLd;ud{l=!u>U{?ahSt{w=uLtObmjO zgkFgXS+3g%!Z2E+*ylFy1=6e8YQsY7eq5`Lti{%-thZQ^;8g?+4|?0SIhG#}j7CO! z$-YZj_|zt2s_XUs^({u?WO!)s=%m%p)0#(KphQ8mt4|Ymw`0mT5IUxj{jkUm~V`bBz3LT<>kr8r|6cyapsbnsHQWJF^6APVn(p9%)WX9Yj3ISSg);g*) zJpDe9MHH72}l@B@}*N?PthCDzAxF??z+3?NAN-IIsT2JyB+O zToyk1CCSMl*9l`{+<+6OOrbcgPKp$pMtN$mJoj!ArXw7)sHV;vOI7xd zpOhzjPh?AkK2@a}Y6Ys2q=8#TI&+aolS5k&3hlR0cy_WON-#` zG-iULMcvNj(zq^cPZYXz;_eyS2sWdra-Ky;q88igb!efc$Q-W4wxcbFHe7c3#6_R zjO8EL)-QAI_nBD38NkFme1>&!g_s!W2e&&aT?Ix;vA>#_WuUj%-<#N9!g=6?qGW4n zY;*H$0?g-FNAbvX^C1U#W*NlyCkC@I3W&uV#vnuH!gR^y4S68IUpmVLEOiP*bW$`J zxPClBTGJY&M{V~0cak?F+7I3%8s0JlQJn>+(!Jk5qvq8wjirOMLQ?URyYwXy5!?awcUJB+PXqYMt=aMT%*0AG zr%$3t!eRFp2qtROBZU|RMgwsv2{WldJ2JG80)xs|if9)HJDD%f+G1{y#1!Kt2$3Q` zj_v)#jMs?5)y9}sJ`FpS{H(p+^Gy313p2C#mU1q^Lo|S~&yPprC`mX7wr@qr=gn|R}6s@jXon>)akkgC89A5n&g za}>i(YQgczC8|A+j!j99%d*K|iqFad#~chOMxvwaACz1zSpoYxtdLQ97IBvukh;tE z&COgzg88Sz`Y%ZSIAQt^K>7XSd)0Z;|IPTST<-c^W;C1}Y^|*R?&1N^l|(2>U(V6U zZ6~VXaL}TU(?Tl+NpBHZ=NDO1IQumz<9eo-S!}hmZp0UrOx%A83W3VT?f??92(f&h zzEuX7i!zw!lgHxV6Sm!{44Kh&5QKPihIUl9F~xC<1mmKVX5`}i>DGm~Aqt%F*94w8 z%{>qzOvyeM^cN3!d@j)A{Y5hhGlHU0{2+<%#W&D@+=BP?3?;eu!L(n}+;ud#8iAq= z+*9CfFfMnmmO2SzF;@48?Q{pP*CVnv!r>+cSH{J_)%e^aL^|+nVbWWzeJAr(u%a61 zMdf_A4@)Vh3B4KwnFFKjafwR<@Ssa$x_S4|D5EJp5wi=z-^I_0sHZX>AECs{L>b+} zi<5iDYpm@sja4@&h`2t!PyXv&K!^>SP+BjLzLyQ*tT?>m4JF z#?DgCD1k7_eKUT;danAt;iy+$`*eE;1C7WU`U}i#Q1@hx+ZG~R{3aJwy$x#uJu_S(0rloVq1xw46UEeS6T_xEpn!bxyBjg63&_Qi^rAKiAuF zf@M^h^)}|B;~;w-INaufJ~i88hmCdiEE)?HrNjf0fl*|^D9dSXTjvmK2VI?LFgSG| zo(OZ+;0ZGE&@kfL46Yo>DRv(S@1{MLSY75Be^{-|i6>MU3p{fN<=He?e< z(GFKuCsB=HEc@zIPN?O^WN+9mPMjuHWx3u6n_r`wO5KR4}%-bu#`V3wrak0Nj9VQyH?r z6-RF0poGg1Sqgt5{+xellI|Q7`VEBT+b{!x(n8Tqw*pP412OS%qLHe1$O5;TjiWe7 zNUBbR_)l(WF1(C2VW!oDA7I5&&pST)P+D11IB)IT3L>gijWYHzer3GMi&B9%azDJV z_w2#phOH}SUiY`ini2^K&LK(1RAF7T2x5sj{Bk5GANcBO+9T zCbUvfUe!`zrd(%wHm!P)DAqLzB15P>Ncm)kduI5~=|}MtatVDrML4w5O}uv}^gX)J zl32|o7~!95NTFpJICny_$@hU*7vf-RTPPKiVR_=&nIK>qw!LTEjvDhYZ>NuNl*+_B zLSfo_qlQ&bKq7bn-P+2}%upU6G$2f3>aA@e2G7qgMiT!?m)-+gK1CLp=<}$bZgPQ< zFC8*sc-M$G^yVpoX)Jjfd$1xrQiUG7;X71CllA5ewH()nl%z4{)q8cOTF41*3N>h0 z4w0LoDFPvo{XNUYQIMXV$Q!;2ff~&vvu<3tRb}whdO++Mh||XdJ5c>5;}^ z;#6-_;eQ~#v9u^C1L#R?Uu2?0l67f|fz|bvjVC{s>W7LPU=f>ma<)G+L z4&;upR4;!y!Cyn=x$+9C$=piw)m(`E-B`2Up@6gXoi)h3g4NIk&~=;wPkQbx=3I^Y z1QU6o10zMfE>?Y(&%e2GrKd-eERc^|$crX!O7#B7lGMtx;ndxITRM^AsQZf||N)NG5HJwP3 zC$rz1PthBP+P_fF$_kYvGR-w|4!fn>c>dKhM&Z_UbzEjZVt%-zbJcnLl4tz){K)V3 zjFe6lKjgvh{WOx2(s#BoH2Wip{@Vp80X2ykutlg?18#7hXX@Ig%0>#sSxmMI<=(y{ zC9pACH|jM@W|Y41tZOFb(?etdxNd%IdPM8T5Q*7VLxIW3IA(JNsAqyZ`#v+x4TdLV z1seX*m0p|f?wh;mj*iVrbkN&UWfls}*{X;k4@fNxLa@{w6=P_(Ho9Ip0$U1XXn>O&HYF{WK$5BW}wr-5xns zWXh)`Dp0IK;Dgy+hrCX4h#T-OZoS514+@ebbO_azq#2D-5~{P+d#7q@BHLGYHcZng zCDreyK^mmO3^>dZny9OWx$EI#u!LdZs$I}4-Fw3syqCw+&HA8RFCymgASOBctg#ln zvscfs?b0rCeL77oT+LmR8n)3CGpNDgwFNF@YjeL!*_TH*gNWHXQPf0?NUXT8UpvM zxng6~iU+C7?jyf7pP}<_azkx_dGmZuY;=IkamJGHWnkUw+zg^NA)AF;uNEwo+#Ou@ z_LAqs#I;UJ@b;~>5V^;7nb*w01G-L|Or`g*HgFc*MpKW}#7I8Z?>1mek|%`JHK4h$ zm3>^k?N+j3Q6@l!&Ra-`m`JM#)xQdt#aq`V3(ezN`5qr3ARFH!5T{`QesFoDW%qM1 z?BLnLEuqAnvBvd*R(ic+DjZ>12$Uw!3URaBk7=x+rN9P?VzC0cpx+M*?79mlhVU~} zau$@KZ@hzqybt?esYp4(Z#yz`kW4!G@k`eq{lXtyNrJ@)WyA1*fhiYz#* zTCdH5$-r&>-G?rb$uKD!`pmq2>;bi)^SqJybT+ALELxK~wbkn@n@vR&2)swW*s^^h z@KIcm(|$$B_~p@5?BWvexc<`CALXb^*k)cb$zkiF$?Y%ZpG{Mvk5Luw(lccGG!bx- z5pCXWgt38lOn^HC#Uijb{HzQri~n>lG86k*apwXkD!tTZS!ymBtYwWLN983cCe`v% z_0N#1dy!DQX4b1LRrKq&5ex}b*%$ZFl;K6}BLD*TpEKI}r2M!YP97`*M7rpg^y zS3Gjuw|$}zJ-U!J)uqQ?g+tWF+-$B27hk8OoGx%bj?2BJ`5&gFt6rP)SAz9_%Ypwl zXQW@b)N+};_)GQ%aK;Omz4_tR(${NI!Zyy<*KS|~0`uNKb2dLuh+KOluB~@O@Du26EH0Qd%V6+IRCX5(zZs% zKR?s@_r$vP{K#L`(CL0v^nZ(4SCauZTOeTle=VzkM}+{-Zx(n}_%B-^K+knJ0oD9| zMqNDu{C%GJuS5M~p5$7@fM<1YzU9O8t9JOK#_O;HD*V0J0dn+LX~6n3FaPKD&OaVM z_UPxC8DJI{@U&K_wwb3#Uf4%xbE*oGgU_Fw9`{f(SqTErF)b z4J#jV(Tl14V&0@c2MC=XteEewLQXYyu->4g`gD*Ti8~L3(ZlQo^4Vdu<-L$?R*zmHJ%mar0CCO0t)d%i^djd2eQlcr0q? z3Wr5M96!i$R5dJBW-4W5U6~fs9pn@wOE1?8+h30E*^^!c^R$_2AP{n6z;ps!vx7RMjXlPUyhsX{%P0cM^%+hs3 zc&TSRoGT=Uh;I4Z)0?d0WXkik&?~;$loaUgATO;@WoaAOy7sz3A$COs1Cdpp#oY-L z0aWKSNskC#8Fl9ZDo3A@4 z;|VdUecM)+w{BkKbFg2bdVh=Gq?Q-r96Zxj`Bb^*oWSRIbCz0N z@@J6Ul4i`?uz8vt9nXhlH=h!0j-_~3t>QavkIEw*4WKyaZn@vr8i?a`@UdzBbSt)F zYNb}OlEL8Z>$-(Sx>n55(Yc81DEwAYYm$RhvGzGfpR(3MaxtBHqqaLuTx)y-8#;9> z=gQ;CN`$&>acYmPP-=67Q#Yr3OKRT0C`_z(&&UXEHo;D%fG)%p4N~|WAq=;}iuIC6 z&|nbz_k>1fyWFxaqdI!3q^PGCzr$F*BKNw;+Se~)blk^RX=EXXu0+XDhD8ONDNa#b z7lT&w6+>M%+-dm71VMT74{;Fe!wgAFcm`~WA^ot;7M5*;cUICG{3^Ag=M@>81K%2{ zFm5)e5vZLd^GTD*~i)k(Elu#-!K6`$IQBMO!Tl)@+YR)&3eWVVIT*HG=A-%z{ z3+d5}P19=S8{+2}D0|w7eFdZC^=<>R*sdE?ozJBrLeM@+mVH!n@}k687x_q+8LF$Q z7*oKDz9F9GEkT1^k7|z|dpEU!UDzkNJ?ReWVo2bfs9S3oVi8=#7@9Q9R=8vpvE*pp zZhROIsk&sIoa!{_3hL?|=L_z!R2;5Wp?3(Lh&>%*pcs@Sg(2ff8;Vr3Vv<&}3#f4r zMv!_*z!Qv$XQHFjdw}KPp_E6A>8mP8CO7yt1c6&>BwRr{b}*8ZLn`v%g`HA=BYn6K zru1}c52lx;?|iFp8+bE6=*P$Bkbupq`-S_rtS=AF2>-DDy&5}+{<>%XPdwo7_3wZs z-qnu&G!XxKul}b?VPJ#|c>X7W_*X4IUkU@&{C=F!Dt@d|c{5IcdHve(yC}%O4A{Qm zbXEA@FIfJ>383e?pc|;-_v6&i^5fmApDtLw_Tb0(=UT)LHx12z?HgiO)%^W}vr(H~|)RS8)Oye!n(-_h+>W zL1#xNTkAi*02uf4MI8XL^*>9>CaCb+&aS>A-a^eDm1#f^ct|HX?nK{U(_;mV z8=9R>+W(#=21mTd-WLS7q5I(-7n~|Q4?35{4vWtOU&JJ8NK0wSI_br|w%)#dd+_?D z-V!1dOh%$a*aHC*a-mG7mM)7P1#8`mJF>lR*Uw+pIwo^gJr;TiF*LQdyqR^P>*L;Z zf(sjok9Z3SDkw|eZ&ytRrg+S05B=rPpgm81=uq=&es~;054OBT-y@g|WZ}ZQOd@QV zK}*dfs6!IvkXg~W+2Z~We4aT!{BmD()tB^XOgDP0wyz&WUQ1UgOj~-S%*R789#8=# z9;m#O;UeFaBu^xtg@ub9k9$I%2e#hcC#$w5dtZjO4k|AeTFqvw(g&&2l%A97I3z0g z4U0cgOf)vpk}HOAeFAjwL(M}bT{zA>GyG)-ov385<$C)nz z-!VHdLoeSl>DkL&QB>Mp-r>v&Dcc{v2l3Wpu;S6ql6|mB{17ImfZDiw3d$_6_*Qe- z^gTw@m+20On})QZuHfRa^fttY<^(2Cj1S2494RF-(?g}iy4jPAKGkpI)>~@_5;P)h z7Uaa3$-FyecNs`G{uIYfTo$r3K&9tet2EPXy*k09<{{#cSiS=drrS*X7*yR5XS1X zd3{SY!G1W`-O*CCF&fW>kghnMCWnI7J={X0h~K+Pncjp%$K_Uy27Q}7?OjW%r_?Ws zPa{dnn3dn?&A4aXu7Av@FV_&R*Lt45v$cHS=ZTVQk)UN=uCpgs9Kz1Pi(L|L6zXv& zS|eJh%p$4qE>{_t+jy1nQ!L%OJvu@F|XlCUJt&8;}GV3#R$vt9$Ze-e$ zR|^*;YxS@ESMA=~DpyAs42wN^2&ZPOTzIHFjijk*K6Cd_MTrPTUZ7`5;QizK)r70$ zLc^5rz=y_>W2jK>`%*@*&|98Tfs-ouPWuG0EXE?W-0$6FhuqPDc~L>MD>em6a#UV zbXJ!1MU8V3mERkA%mJ+PbrD45l793>U+_x9GB;2htu92n8Kjm76+3eRBnIj1LVXLD z^k93~h0z*_WP6y93wAT;^2o^<9Z+eJf{#a)T1R2gpZ3zWpBfWp1t{Br@v-#J+T?0t zIFF{3xYF07^Ay8dVPfJ63p-a}eeB&XQg(?#Z;H^C;0Xd*DtTfk`6##D<>|P~$!4hu zS6khYhWQ7r$!E4L&yiG}EU`Y$d_vntI>0nr7B6kady>^K5Bo-Czc}k+7O_FiDO#Ra(Uf#d_$4>Eo@8|*fm0xxAfJt88+{W19+id@9 ze!pBr)AFqdn)hV}LV&=?ZpC^H5`oAlEUFS;3&6f_Qj(aXN;nVnLg|?!=qy7Df9MN^Z@S$=lp|c|u*;nmZP!%@YTnFGinm zLf%b{fGk38=@znldcqtT^F;FC#>MtRGnCZJ9A`SgWBNwDRPx}xnmEOly(d8Kt2m>1uOa4xkp<)r4$OvD;? zHgrBIqh_gw%@&V?U^egDyVD>7yI{KH%pb!tb0!{Rt;F`r=@H~{jHV2F8Gj}{#F{ZH z!`znT)rwnqfK~HuN|}OMF372wt}w$9f?F%Rul0HSTnk6FMWOJ$Ck*gjMjf~?&9ZF_ zsQsZI(CP%ZG%EaIxVDSY##-JOErGcT<*%qvF85I18m@T6B|J0q!Hl6I+IV-JN{=-a z16zQi;K)2EKB)3peNBHiOpfsBEiw5#BAjy9HjM0%Q^B&-JYki($c*^OFo=}J0hUjj z4iZ{SfP(hwwKp<2XK>C;YA`@cp8V>&N%i)wOqO$SK>lEWfH#Et(lO87 z=$NJcJ;(fSIkDgGr>~YkBmJ45{mz zJBwMd8Sm}o&HNC6EC93PwH;nDH6SAhvl9@0BKY`rMp(ptD3SkX~<(KkB?Le*{$d`|%@7|FL+<_ce}w?9FAR%byZ$9nRPc*#3YFVD(s6WK;Zf zZ2pw}ZFzES`g8133$T6Da{mq~Nsr#Q=%MYl1(|H|!K$X82zw3_Ce4o(=7JCC$_Ea)($($q5VN=3{=Lh6IOS9}PAI96@7B&e>=gCHJiJTyf1jLr@u$F@)Udv6m z$B!EgX8H?AMRQTF^&$J-1CbKFtvJmmr^OV{K{^=~c~%%d3w+zz5g0@CQ z@Nrwz^9?xS%3;DVpL-oo)Kb_$(gd)r2-OuBdpxo3Jy#eT;wr_$et<}dm=SUg7J+Dt z4{uMEZXA90`NAz)HnllVF6Ftk8m&ye_pMH@8-534y9s2N3l8URF$}-pM`^uCi^U+J zO-ximH|2iOijonaA+%T4eLkTP+|Cf9&L1}|EP*~-zIxu6(kh=YKkOVWZmc65cgEyo z|8Du{v4bZT1`E# zy(>nlRO@4D^(dM+ zd)^I;sg{o)gKk5@SIt`OcLvg}b@B45Q*%FGQo{kyOj1x$@;$%D}_Hg=|Nf z?}5Pc{k6TdBB9Z=vEp~{CaS80ij25%(pm`8#Gl!coVsYsDVn2vn96YbSkTd07}l8c z1S;yTof+UOdCc^XQ&#B^7Z=t943w zD`bw+HPzsEwi-&#&o)AYsIbhXRlPem;dw(4r`&QY+_re!huVhdXpy4Nc8~_^Z9k6= zrcG&LVX0~eR5uIoewuP8kJ@jaq8yAM=~j)DTWO&ag;`T{Xd`*|=HM>$#yj)dx#XVP ze(Y0dxNR&vBlS&~tNAOT631=Mhx8ajJo_h|C!Xq%$54_u|SG$ipm=}OPBYSl&@gge3866MHE$*^E?#BJ$c zr*N1dw0HDCoU*F4-tt3BCzpDh=jzK~ATNP#8i0E%=d-_xm)~;+v>3`S)moc$Q@f*a zTI_Vuh1_%nh7MqYLtOSqU8OI$O$>8z*tL+B@as!Si^y8wCto1F%)F!Ygd`WUi^mis zuWMI+&yP)e4Bs0}HgE|&wb@;-hx)?-r0}X$QlFawnCSyKeoP)vI`cbh8T$R4nimQW zz*0F2*JeG>-p3w>c#!0YK7CEbvba~W-)>B};g+GV*KYeTq3lUu3KA5r9e>pp%a@RD z{S(7IA~lZIw|d)_Wj@S@cm}n%#1uH3$ewPyQk^tC{W$lI+6A4dpP#K69{2SOTXgHk zb^C~QbAs_&)mNppTLI*q4v!I9<6h+*_gH+{zlfZ7^k)IFiaE2n&<;P_pg=J(lkcaf zIkO>8HTG@tW&MP}2tlONo~x7V&to8owYP|jei3jgzb3z?3s`nXVJX|PZ6RlCYxl3K#h*{-!0QQs=bx1;>?ntZ%J+t3#fQfJC|d#F zLY4>t9rVHE7#h4;4`nrX;#uqjTEJt805;9lC+e@aO{Sm%PPrlPg5Ndt0(bNhWa5cd zs2F_ZdzyHyLmkkl*!X1gb@6sX;p)&U9^3Z~6i;k4b}U|oPTw{K ziBNE3a(kJ#X7&7%aN8loqiPvkb(-<0Yf$@_xw_8!Z=KdXMMBaJJflHG6yX zB(wywB*Ny`5>KnMJ4vfa`B$Yfm0r#qqMx>pwv9 zX7>EIa}|IX?`k=@n_@hX%Z-)4jqw1b3OD~OS?ColEdkDtEOOu#YVbuINgf#jQzS1K z=PB7HbqXWK-pHt;AsVdPE&Hvhh{jYwqk;&m_;>S@s$24M8r0>W!@fz>7@=k-lfGM| z^>@(>$#~isJ|(BdiBTwZ?qR1^9=v~B?|$OHq#up+#i_k=*UvkM%EdSsEG|qEBmepI zt;m6zRz?xgAU%?rtnM&hR;Dr-;vvhhYQZI#n#2YpnGrwkZ7Zo9*ORspZ$weZX`52J zj49j)!jVBwF*Fftp;*(`>CTZPWl`-{AB`c!6hG!MfVj9I&?cta9G!{}&DSVF2l$%K zQPtGtOQIBmeovOGxc;KA`9~1{@7eNw>9VWL7q`AFd-IRzQXj8vd7B?CaO*%7&tsA= zEkYR?3{IKapH(Ckqb_X1vm>_+Y zSvPIW3gv)omSf9(n9Fn|+#II>HrhcB{~k)DwozZ3bj`5e3gole*-7=-7yZ+ED-~fV zeHdlRe5Yo?-6b=&pn3F3$*neIHpwAr_M|MqibixnlM}?tf&E%9UJa1w=L>PaHdp3n zmOZnzPC+_P$_vvN@bQ6r^I&Ar`Y>yRkJ~bIZb$e`=>hjfo#xWC?vbeV&U=>UEEg-4 z;_J{6!}gNOZCM`aI%2*>)6bTN^92lq%0b_($IM)K$otH5aDeP=HeVRnK1Squy-7Y+ zCT=uzp$=Jbsynv<%gfw(zu2Wc3m2+@)D*48_~C=caeJqq4Ny&~KtKzOHMM9WDcHxBkkoA%lsOA`-puwF|p-Lg#7|S+>_Zt5FmL$IQ?JA-UR7l1S z&usSVITgl?H=acXmxmf<&mUd6kePh+twHwD;jfQnclx2##n%htfjW*m9DhENnR$%4*YhAr7H6{izX? z&`21D53OyJi}5jcW9Qk}?`%LfMnsboSvmog^2pE7R zukz!@BrbmxEO6@tZPmfABI7m+oX%RZAq7mLc1U;~hjn+0iBX}IAQW$q+g3SS`y_rB zb9qGq!^BGN9?8W~$9*Du?Ez=da|BGF);23ayd8@m9Oy#VZ$Q>iWmtLv#*@np|H$5(6kT*O z6-3d(mstteB z3^Bi6u-y)N(WH&o9*SIYXXg88T4a}5yjaIdA_|f9O`*`CDm?^vcgq$2>A?CU6msa+ z!N9!*sUoYdj-l0DNxA7^z)Q6k+;1R470?ZJYC;w|#06$l7S-i{8>GEe- zzGknDt*oeT4ts&@wT@tb>*7NeKH=$!4^2iyfw1-yuBw8mUL?7Vt;SZvg(W=h3Cbmj zq_>gshUk@$qz}vPc@TMiG%BIw8x2dEWQ$r-D}xCS)TTfx{Nsc-%tHlr;%8jZGr0yd zME3;`p0sU6Z8`_c@u7(@VGkSE!5kN7pXD@sgtRmu+J0u1o^;C6CHyeTut%v=UY&wl zT7p>$Jx5!hKXGugabp)U4^>;A@?aGe?}G(5g2q@V5`YCKB!fzom+=Z9X8Cz=%Xf*` zU$!Lw3f&*S_3&?i_rK=^;8$~g&IxfFr(dLiSJZvUZa%Eug9o8WGO&pd|Bt!1jtV)d524dN+5}|JL#&la{EXSTfq+COK$=_uS*Be$X>tt zjF*&AqZWGt~xXQQP)|N zSv`k@dI8YN9LO7WatLKdk_JZ!4|)}U74Z>7P5#|NB&U7i z@gqU_1H;5)r7{1pijvRd=J+v&HtuTN-w)Y0tndrD8}!Pt^y4|?*3=v^&>gfEb&SFb z`O_mY;`4)$w0@ON{t%3gJU!es-}Vf@{wY9Qt>Du{-EH~V!I#xQeUJ}&<3oq_3^$Q29)>kpj}mEPS)G_EaxCeV&)tGUUT^L1{@94Z7w zl1s%v95VYi=8n#yo20cu;mP@%py4zjq440n=bC^H=FcyX?w-HooAp;h=rzky+4%sovRq4U>%DUDs_G|T8=@Be6@m5WeE3OC z7{#UY3j}_Aw`=*a6Y%0;n44?wt!u}XDc1Lx=gA>T2=6m3t?`@5^!D55!OI7hS8e*Z z?e0t~)b2r%1!315q2zOj$c-GvvCv}u&Gm`t9YmA|YGouQ5iG}Rb(nctp=cHC2|lsn zh&cp=U9ZFBDRtG}EfQ;lrbKZr{{9qK`>@C)8KSuFsKlM^*~*%)bQ&{AtdiD`>6bCk z2;R@;*GtzBFx|kBFovkUdW%%XeNqa8uC6lLT_jLq6bF@4Hu(m zEiW=bp1-rv#bZy2fw+;Ub74fRJf9wdhHPVsp;G!t^+KZhJNBMT-e?~9BbQv8Ik)E$ zPNxkI4Hq@Ak*?K{N?WiS_Ica(g2k-bNkbbTH-0yZr?1Z&9@j-E2(?hRU9Tav=?a|& zvhSUKPc$}4_v4P+MH}p9zhUSV$Y0&@KMhF!^zHcdHUCeNbUPEyZ;JJ&Oypx9%tgV1 zF<q-Wm%G$_C!w4N9sD#;%4Q6mz1q7f3#l?5J3B=oHA~y^w zR*YhRjVPQBPRnb$%B#C9K3P_wb)SC20~&Hm#fr#ljCZN_>R2mYJe`@5Y(;7r1iZc@ zXnUWLqiejOD}W=gVpG$&`uWRf%{admp|oL{0g-G0-~wT^Y5dy6Mo{(V^OiyJxb|A8 z5SCoKR8idXp4^n%JMr?&68!~PK%>ad(53g4(Xd7>lRS8E0tWQPyDoc;S|LOcyyZzx zv+SRvOac`-)}2|vT&L%h9Ds@OLK6$uLa+tKZ}MjVSctVB?PE??hk*Sq8Q#5R=oPkC2K6CIxy;L={j)7r*;m)=#qQRxRPPr&N6#ey#QxKbiV98jvRCdUDuD@}>|iLP3#DSJcv}4zq&%Q-5_$**2r+ z56afm4P9u&yk~>+q==%+4ohp2Ug@)!L?esnSSfxBKn&r6`GwHq1*j&d&KO$#r{BPA zGD8J7{Ip`sYvd)XGqj44>{$2}(p>e6CRU41)BMweo1l6oxFG9#S1E5AXvfyYW#rW; z^K|i%ZiqYWyrSeai`5@FqI}b{84oXuL#4kYowr`|Lxmmin0mpVPE$XoZL_U}vf-z> zQh!u#<7Q^DNx#iYL!P{XQ-@S40;Vxnx}kHI^&iZ%x^ME0syHw3n0O|wY-P)%ES5j$ zlcAk3{p{aUCx{m8h#BRXR%zINB_4R~>mbd?Enf8OB>(Ya8!QICiYT~teGGCzuEuD) z^66JjwdSltFzFgOm5>GP>QX+pyqD;4W;kTYCKh)HIRW7@2;$%iT-#yx<044O$I{Cp zv73ARXS^SdZ0JKKnaW#Mr4IgkNA`2Q;V(F{KY6Kt?a2No_~M_x>0j~1e`f+zB-AWy zjqU!df3lHd_yMKY&eYx;4Wvg@DRonDh6d=DMk8fF;}*8qyRn3$;FO2D$Xi<^lEe8o zbCA=%yy0kgU8IVnhv0y_PPa2Jyg>+~4_}s9VR_J|unCJhsXwWxamrfx*|Mi6hfjo2 zg@Lz)g#--GjrxwU^Sn1ol8xP{6PDv(I;Hc1QW*!ifB=xKk=&O@R@>2WW*Y zO>`(F7OvuIWp-YBt8X07rU7KeUCPu#)5H){DwgQQK6!Xdfjc>u@^m{xvVovW*@(=H zZI(4R0UA&ahLxC))0u@jkR-bgq@H4TwO%ng;7qhHjKIxJ=mBtTHnB0Ks9D%6n zJ72!kqH#rDM|1ccKQP*RE6;K~I%Jt@D(c8rFD_=71o#M*XCB14-=jOPywZf0crS0F z*VPm&a>&Z4rS3$u9Gs5*f0y2L|M9bo;kmfzQ>eX4- ze82GNJJ62+SRwKdLH1^KEB`6W`zvSlf8X*L-mcWIywMM%`+Jw)FVOlAmz4FU7vo(= z{WcJ1%@$bj69RvDpeZVBm zdw$;+lUJ-u$Co%&I&@!Q+P?JVe3KFbP}z*{NfdtNj__$ie!eFA3aDXGm1)(enr=3= zgr#Zp5(q8gsj1S%^}2MXYcM*gRry)Ao}i_n^UFpl;`>c4P(R~RmKb6#u-u$5eXZd@ zaS}nXxFeuOgk{>$)em6+kbe2V@QRlY7yNNmFsy`09>gHAH0oll`9;#kS^1I}iK zh+XUMKLf&4l(6CC0ck(1| z(_quJc?i_dCMA3Y+h6v?rg*So-B_9nXHV<{b5-*5!95ZUZkAy(4lU5J`(d4Yetvxw zVeAN9>XG1wAx>P(4*=4l`u1a#qLU&UHM4;3%XU|ahs`OO#HpD_-{x@*{VYOul_}5Z zw&^0^z0|>N9B<#JeK_3|No^dG7665qO2A3>Ft^{OousNqhm(#mQ|j1BeAbPn=T*x$ zq5pV;FaXwfA)}FNOzV&L$r^kPc8dzT6SB-Lfv(UgPI#w#{uIZ2fOH>0)j>_CNC~$* zN}jEJ0tj-H)2%bN2e>j#-`I*V+lK;6CQwZi^eaQ{j5^J@@7r+kZfgi>b=j>Kh4mDw znqePaz19lnh{Y;O@p7fDub%)0C#>f_+_NWE!Z@wfr3eaahqW3u)j2JK9?v(jW;{=l zY4SIQodg;}e<=&JjScGmW;idK#79$?k>qIasLzGP%4M8Tn_G+SSPt_2Jv1%!san#i zqo3)Z1@f8Lb2Vn(*aXAl&6@_oso32M=c*T{*Z072-L6a?AAZxCY~hVpdaoyHPaogg z{hs3)KOVI@dC|$(BV~EKS_EUSd(1AXV(v^N4kO}+-KFE33K&O*(JS?d&A84UlTAe# z^l-HaUE-O(fW9`j{O2aj&G#0fuN~a)znk7}>EB+R5iffouKBiV(>jhfIk{E^dbVp| z-K)%{GKmvnI@NkT7#SJNhtrWau{#~1Gn@ckNxCJbb)kZccO^dW6y>pPAp`<6p)-Aq zDqqFw&iEV)$p<+TM7`D$rWC?=D)MM2OOwg?dT0`a;pI9rx{-qhS}tat&tt?jK&7#J z0b)xG3v8Q#!rCQkczq^S+1=N{S8JI6+$C}Ss#M7sJveDAZlNjB#c#j@I^LxI9^()y*mgftUIhc(GO#S21zaFJLj-F_=j(ASdU z*MNjHYu2_kp3oNJv)zod=ZAIo>|GCl5U&$$$w}L`r9haX+Y>^};1IXrZTYliWQfSO zDIfGfTX!NC+m+uPAv1fv4SwUX+wN?-@21H^lNiz#7ipC%H2Io>R3AycN)6Rs$Ip|! zRf#ije{^&ev2_LB{NV0!^9&D?=?G{NYL~<8o>~vVNWDVNDGM^CQAu!%D7?F(wzE9r zpU0UhkyLuYK*8?PH~UrDZ!E+RlEQ#^VlFmQf6+fz%V!Z8X)F%jZ7m8dl3qeo5j$E1 zk7xA=iWxVK+-f_pwgZKvIkEui6Lw-0vT^CQ2w0)v1VyVlSPMex2&M?r*kze)OZj)W zy!phc2-gB(tx3rzdB2m~mA<%!7JN!2yj5gscWB0>><#LTG^I>UXr5U==ddvgD>Je4 zhEd-<_{nhFlR9@ur2s<7L5-ZIu@+OxSQL(GUs`D$rp2qu>vnG@d*lYn+`Q_etNN{@ znj<$e`F3x;_InQ@HS*|3Va0Z$BK!btL#^au_`EueSs)uQw!pz1K*>*&oQG{3>`C>e z%)%=1wA+Lt^DTaiOj+O>Aq$~m<{u6kwM5mj(n&rRUjw2updB5mUU-=(tzem8pF5@v zKF(K)OrOX{8%rGs(o=hqlX|GOp`*dbe6&|ed{^_X(vLZxlD{u~#EV3CPigzpr$8&y z(ho9DEgG#gn}9Q%zJ5W(i1{DLCDesTik8$yZHz|ATv{m+^nV;=5eh>1*to))}7MPZzT~GD5u-Q`=Jf~?-MR1 zXGaTLvp?6t;S$p$`6lc5qyI=q?qb@y3SY4nodq4VXH(;gscJGQ6JwgZ2-I8m(cR%d z)2L9hL^zATQ~K+M-QHf)Tdaw#0Pz$*k~v?-Mwh*Ma>c5?BKO*po~A&ve}y?&@Or#h z?(_yi9yhxp;^O)kGGq~wF3(d(ZoDf0m6;whKrUQ)$R!1s4s7dtu&Vr>EU9+Z(A5Z_ zbc#`V1`=v0n>`dTvu7wp>PKZ+RFjaWeZAvQsXI!avX9rV_9QjhVR0>oDa0t^Snv5Y zx{W8?QrHr_19MpgeQ9A}k@2W#ThV|!S+uTTe`M%m6wwDeZ`_dgTlD$&_4FT!?4QC; ze|C$1ZB_ppap#Yi!XNGZq829B#(xlc{)U9!(()ggGrm)L4!8lL@A}GlgqMZJR0A6& zQ1Mo*R=<;~!!p!GnX{_+8x*3XLTIWBnGux>5U3QI1j!1Fz2vQKzZ&eVvkyv*e<+`Q zihXpLe9m$@J9|%@AYsA?0L^rDoOU&=Px6dBpvwXkz4~(5^R&7OB(rNdYM9>V?5+s- z{_N`kR;OivMzDm-p2*(l=Tn|F z9cagF_pV8HcvQgsT7*|)lsjmqoh*x%(K*gU&0w9@I`4|ADwy^?5!oMfxR*n8S57(- zPR~0u&z>OcSNi&Rbgj;HQo3#d*NkSdFY9U*Cj*L)^y>YuQbh~2rDKBZ2s@FS^jo^< zo&u)w&dgi&Eu^TSmFDWH&kA!)1m1;&Kk2YIn=wG&k)Fo%5i*5GM5~t?88J-zBj*p) ziuG@#auOx=D9=b%XbvP>PARD^l*F%*3C1Qcg4Z}Db7X(6#uWuL#AO(Gpxl}0#RZG4K%&wgDec-aa%R$axA(u^}P zlJChDBa!CDLmk2-m{5qjx>yqttb6`4phtT?AH~7n!ot2tSEWE3mcD2mv!R^4V$*JY zzWnJ%QLsd)OGv6OFbT{vzIr$ zHKJ&wD5%L@WNEzGHfaWFjK-VtecGD{ z5gZ{U#jy&4E#rcUL#J3vU%rBb>{sQ^^R~*CtzMK15r{(vLoBz2_PeoB$(Zs>U z#OYVfLqAlt@c&C)OGifF$J34WDXL{He|q2uT_D1+eF~5zVt9WAu^YHZUc&(;vDd8!fr&@UugX7?ZLC|vA<{vV4g-vy z@RF=)fKa2lyP$h?6v-ApzsAMTw}K1MyT9HYT2uljw{hu#$@MVDn<392-o!(@2G=Rg zrx^cB(4tkv6>&e-H-$yYp3H;sbqm(iWb5F$3JfuSR67+N3SFso7o+(nYaVKpAVe#nH_KsM3KL z!>>O%XzoCLs!olJu6a_y3|$2K=?b{L8iiy>A(fEXU;+@l0gT4EK14ax%J^BB(iu`h z+Rd1GxU&+7T)X1rh(Ac8{ zcuCtOG40b=T#Suem)+D`bL!q+L+w`;a3Tv6{~H&nA@VxPV|JS=(iM{se+LT{pA9`U zy^X`ztKH+dplVciA7TWKcY%>}mm$aDu!rYZOx|25>MO1^S)IB(m%oYK7vVe>XIBcHgu}T`??AIG5UM4w})` z#pbKa+NGO^FrAlW%g-gyCS_gTS7Ji&$$;14guQv59EHuJjr4pD&`6SL0y&il5_v?` zMiShPvGRmaH+T%NS#2iDWufMFVe*wfD}o=M=g3T8ErS1!?aP#ZZv;Q;-V1f9FCzJ| z1|K9PNe*L6>yEj?BK)J*^ux6dX;5mxCOHxaZSy*kAx%r_+W<9_K`{C!oL$g4duSWM z=YmGnB#KZd4wGf>+SXa)bb5x@xiSqnCQ^-j1~4DzlNDJh`dudbm((1M7$)vV7}Xd)MH_3o;EU zB&Zc-VPivxI6whYgr;o3n_U!H-uESz4MvJXdBLvo0SEV(?}zW-%H! z(%(YF-`)gEk5JOkO32LoS3^Za%g+rJ!nuS$UIhP(ehMKWZz89EdJ$YbA09v7z9pT@ zY3A0H`P7dTWJwE(PcB8RC?2?M6+{XS#8E~9B@#xGb`K$;m7CTxB2rf7;n@=Z~l&GK1x0d3%|s?ch!H40#|=Z@f+HEz~Cu{Ps3q7 zOm?AUAeFo-wY{{w>CEEdShV*=5*%d(Wk$>{-AdrUHEbx6Q#Rwi8D;5zhVA(a=ksf; z{NKpSe=i67H^BPeLUKPwk1KoFoBZt$zvgoOmYV;~;3TLeV9Kb)VaZ47M&u@^{_82a z)IZtXpMQ$}yG9<`*xP@6kM1ftb`<2a-bmwK?>3pyqJ^?Po(Y_2>A_?*x>DwZ@*8?L4T(4hfskksY`;!(A#)LH)LW|E zNO5m--L1wroeC!_)xpqf(C&qu>?={VHd3}&iN=LK%aP^4>|I@br%hIhu14wrkUHvK z2RXizaa{&k@sCru_v7Xyb!baepj?6TyHBHoiSK=fnaFzw6{tny`K^8pY>fv<)qB8_ zH_#_2O9!ZA;Q+AzN9a>YeKI%EAr7SBz(W`|)Ua4H5)_ zlg~tCm8yWQ>o9V67Z6;XAg)iDFZ0HqRGX=uKIY28QyP4NvmOF>Z499D)D5Wh6u5KU z*AWtCw;_SIZ{~rB{94$soF)BQXXk1Ww#Rp0M-+qr4qA_3jno@S zsn)BUgvfj^G1IoF^C8a1A`(tbo0$%)PS~$gyo_LFvQu$-pU`;N4@_W?++QwI_~}-T zk7M68EiPT9azT1La#iB)@H2>yyc>h_mtyQ3F=RP*oE?GZeKNo9 z86Gk+B9atb0A<)4ZDl!2&1%?M*0-2FgG}r!VZJM^q>|Q!^K=LlUF>vA-ufuZJ>=*V z@Z#6$9)wjG0!;6Q8Vr++XqAc^CtCEeA^sgu;{wFpYj+L+${0N=4xW$u7TMRyW#K8m zi@+2h(ynwwrvbbAt^jj!@?x7BPZQ9R0Yp+nTi3Az@#pSSmyB z0?y=p@G9o!kd()-XtH^8M6Nevj1*99Au`m>dA^YO20-opsq(K&Csp=b`&mIWsvQUK z)NwlYHN@d@=548uq|t)$TuJ6I^`FAnjSrdTGHE(MLS>S9K2tylO)<bjI^pnK zOe>+E$#J{L*ISe-e<& zdJ3C-K-u-}q6Giv9)>DaegA(3q(9HK|Dg-}bD`(=U;p;Iu!*&anSry(zjygR#69-^ zOWdOh<5dn@<>~GPb?QMXqc)(fWnz$n3#$HLA0i2p3H{RZZkdlyEQ-M%q=GHeZdCx*%gYg`hhR8JF7OYj{a6%ztstJ}d~kf2h(tpTTdkYBb}5XPA- zcoQ&*jA`zknR6}&A)VOvGizpQKdo1Cv^_W#LxoRK0da7wKoq2kB8%KEI1Ws4 z$GI)3EudSp%htBDjipvhvEl;LHWkuTxI}pcDC($M!4iQV3dN~ZCl&Q`^QAsY*dFKf!ncZI~hbN9RjDGXA!rn0YcHV24O z7k8isp?Gy3c!f{!DjN36lSm}>k!Nx<*w{;cL}gnl38GQZ5ZHI?x)DEG1)-ql#* z61f;6(%X9#D|Au>K|ohGKf}PJtT-zxvek*o zvb{1wE48|@x59TJhCKxC))uKC_A=M{`u*Hl3IYrMHYNtny@P`8`1FG8hP7;aLpOK3 zukGbD<^>D@@9b)7;r=lDwGVpU66bQwwBlpL_ffR17}f8GOi9;uPiqW*3v8x&hWnQ! zJoJ@C-o;_E`#^r*QG+HCMQU>MWx@A^%n>D_l;y>x9bT%(mB7{yIVB3*AX3{&x7Y{* zPJ?A&KR2F)D>d*GCrDU-g)&Z68i>TeB6fJ>&P!{ImQg`siD`2ujbBR!TzS0VfzYbu zy3FAgs(SR=?BwZOi9N?EfF_J80tKH36*1Nn?M9f0BN2b1%GNIrE6!7>ur@0jXBtnY z-H1eIWE(6o8>-$Ngg%@2R(GKV!Ih#ggGwS#K%U*Z6R3~E0yvK$RU`0j60;i$L$vzC zqI~dHUZ(kY;ihEb7+5R8OYNy|SGFh~>82&)`o|?k!eXITVu%qD6VaC`UqYXAwhaw6 z$iS9`waH>2B~h$Jdi;h?&MhmMe(a+cqtM&EqbgCQ9k`A49XVeMmd0q`71*8RA-N`e zc+DtrT#+Yd&sPO!C5KkR$ha7ut>)*lT&~jIT9QE|3FHfI0$7C0(#yuN~w& ztRU&$V%8RBQ0s>DJ!O?;rZ_Gh;xgcy^1fHg?k6OqJAcaq^J#e+;P6ycf1gb973s!< zQuYz`Kz0!8Qi=IpKzo2~-PM%TxtWUobLmT70w*wC$s)!C%Enpk&m~>1o9lu5IYQ>K z;cLzhU^iW{nnTI5<7y;Mm>HA4h@6g$@QSXhRC!0_(rCuJ2 zfGfCePL|q1y}LsReL0Ef+YYQW*YTx9hM;@wNGoSa4UvByn&C87+ckU%8YLNt93?G( zpCI?u=HOWxMF29m*5A)OMgF}pF^eZdQHO^saS(%S(o_j;CN@t+SD9Y--c-`0o_Nw^ z!t8+4@^BCv?I2fCQAC$20qwCvu=;qdhO+>&LbfS!hI2QZGd~+TWOf4b6-}WS9ZcTR zsiD>dJ}(KY=Lu3>DdvtiyRjG(M6$G6jcdZmDET|?5N4dzaEya&Vn|N{X}y8vdZ(w)&9!D^>w66!>KZea*W57#|Es5D7pS6l1Y_4rVG`-gB-sR*hPd z3(nGdgGdQ!3QM5?oU9XFKJBOr8_P|DlEPq!%pbCWhVc<@?2?A-qb8B^Y5O^J-Pm6> zP@gpoP|hsF`Eb}k*_%?PoTYKDjD{14Ba(>sil40vKF!-v9I0a+ z&170Hd`&tIAs!8GAEPO5Kday5xQgo(N;WTod5i{iyzhT*~CcraZS)FbUncyUeNc*46bMkkj1MI2g zT3WmBLf8?34ozU0Nt5G3bJB)oq&x;`v>C%@lI#Q6Kj5;}9>fJoHq2#glOd5`>w*U> z-_r%Eb6|G~POVvaUj>c7h_yL_gVPpib1~QT{nqcY3nd{| zlMwa6isSY-PCLtH{AS? ze9PSb``^D}KYsr~N=7EOCV%`vwWsRlho-vb2XCuN!$A&v1obsXj=`aDw0d07v0V03 zv=DJD6(Ir5;0T^d5ZKuw?#K1D)5x#p@L^Um!UQGXl*Kb42+IrHY=V^I*i zcy_@EaX76I@f+svx}dakKy?WVlUcZ6r-W8WS~6Ep z%PQc;8eh&_%)Bw*JBM-|opVy6S;xRE(@bU1`G|B*hwzQFQKFp?23K*#`3p1F^$H?L zj>>T!ON-{=j7;<}N$uA9e6KcS6kM!*%z3a8qlk{xl1hKSr;o%lIuMBvqHAsCO0-P^ zrJgeaQKZ7qX;sSS=1`oJKHuQ-aFossH6|X#ZCA%K1S}*=oZ~JiIIh`Uvla41o>Sgu zU?^N3IM?XsF74dYZL$}>6&|nzdjwbXfYYeDxyy3moR4mn<i%sup#6Y*TsP;p+SUGUrR zL!u3DZX~T12=VQka<3Zp6F;DoOOIG1B^UVlEC(Nx&BFTj>#VeF>* z_4Y9CeR-Ro?mX<>qx~1&y{;ffAsI&$W;vD#q)Bg!SXZsO9L`s}tKtfyBh?L|b6(>Z>YxEcThRM;3%Z zCRTXJI$1=eyYxC?Y7W&O;x;U3$Mx4_R3|TTI_tCkCqL<*6Ck}c~14HjY#1)DQ z#5>W=;Dz8&c*;SuB0Zo2Wd1a#WzmA<0Qs&}A zby|VbHMPn$1@y6A-rL_*=`BLWl#Dshd-MI*|-CfYqBVb-heAA}SG+A4)c*U8R#D9NJd^ACXbNS`%& zNsv94_C(Zvi_$aKDg}gGhI0g%iU*v!1M|H=lzEo?Y`m<45&J_yg!#vLJCXBR+HMlO z)m?r@R1)pqYb(dbo8d=TX}SzS1LK2GkRLEVYHxiEl?1b!@{1)SAiU2adzr+g=DqFe zK$tw`^_*Mdo11(q4wncvAD;HjL)OikO5310bsWv8C^QeJVxFGjm7gNqftt7hz<-{} zgPe%7N-eVqT`KX*AnDKp^u8Eq(#ru?)M;er z?}t_~=sNQs>8Zqh+3(&u!G*k0(my-N=t)VcPchp|p@4)QCRv#lZ;op*B=yAE^~(^A z=G(0kDgX=1lY8}@yWV&4d>&eWp&Pc9Hk218U5-qp^!SpWIlfI==xu9i`BA%?nxZ8~ z;C}lXR604;r#ACB=KF*}#?sgt4qRt*e5MJYGIox#Uimge#=s*0D(2EgZg@whI98Eu zE*dSCfIjh^%BJut#!nSHgK~Vhav#gTtH8nBCzNN%N+pW(>?Tl4-drH^$XlkX792$J z3h<5W)5T}>k;J9wQsh&0+}R2ob#HU9KEsZ{Th1cfX&URV#E%oYsDJ$Q(FZ>EbD*}) zJv`J|h2`s4O80ZEq-(3YrDbjcQU)X4X8$F}L&#uflQbU@em=p&5q)BtX)kV({^K?w zp1Md(4SbmpF0&c=D~dT{qK^>(3WX^yFhGRNZf13<=}Q%VW=OWE*!bZO&mTaaaaF1T0YuCEIe z#_Ss6nVQ)eom~Hf`~Bg#o!uk2-b35U%jw5B{`P*NITs`WCD-u<8Z%6-dp$0BevC05 zb8=yiToMIBul&^dZo@%5eq=~9qU<53&{E`97SZPP1vm2B3gtdq zmN2mjZyU^Rtxs{WD{kVbwYZqkW=QOECFl5?W4A_l5HUg zmw$(9jGaSenK%KV?)0MOHTwoKU zZg{3C*mx9&brHC5-zq;8yL=~ZXopevtY|u>TWFVuwO0=_ii!vgno+I`Y?oI=d`=7~ z`ZS0zvCEXhU6>BWI$?j+Y})s<%4qR6LLP{djsz94(L(8|P(41$P(hR~l{#UgSXCL{ zCi3YwDEisAqmucpX{-|G_k7&XfbEx+mER*bTD`xB-F^v18R+R70WI)z9KeAUx*3Rx zA$oxh^o;=-8*?cPg2SQd>BUL*_6|wVP<9FrcZtzb646SE3yG5pDZxR6CJ+FTm@qNM z5f2@eq>%jHx*O}fb>1T-C^~IHUqO!{&DBxXjg7_6Qbm+gQaUU=`WibNsqP1eCZj4PQ%}LT>C1nGtrH1w7Jl&l$ z=4dje8K!72S$)f%gEowDs(zfl-2)jroLz1SHL87nFBmW z(^nz0;FYc20pWOBnQVY zQ+UQyVz*W689Ed_8e;&ORb08TM^&7Vhy@!1}R>VJh}H2J0Ug>JVz%ne8ZSH2{Hz}eK(p4va&Y5R4IKYJ(zslnLp#i#TDBsbCRVs2w$_hHuUN=Elki z6N++uDbA5ihHWGLk6~Eg?OuAE^Xz$CBzhZGuQ(YWrn&n2a$dpF(9l}@7DC`W9co#rtli3!sdk&g1dee0S_=#Igh0;oHTbReikFEu&8uN@*Um#84?(2Y|Kz)64Z=xz*G;NxPXy>3luWT{MI61a7whGT(!@W@hK;y!6w zRDE!x{9skl-6Dj8h2VW>ahXpSrMsO=Xw@1{tg)aE+8iV^iNHNb)5)IFb!b!&q?TmkLor<0)Mokfixt<`pkC0jfeJUt-;U zXxX-_9mnSL&eLUQaX3|Pv`7;oN)RKYcaz>L5=bSqio)usgb}1kNk)hFSxCX{d)`YLUQ2EjR3MS;z&F*eHGdGvS_EEHuK}x4?G)ZU$AdIZDJ?al8n4cdcJ5` z@A-9nG_`AJ&VuZDKr)19Tj~=VUa#qavPNDZBCoLGAkkwS793q-fb58-@(g>}KqirN zv1yfV=>BFrQs^nrXF?KFmT!f;c-|kCn^+5P#f*v4lda1#4hdV1lRV zdX>l$bF$2viBtbT@pJ-SiN*g}ze}aZUr&i zY6hUJ25QDP05ej8ShH0xKL1Vdc(b9d=423LJ1oyQ3}pgSVH{N0BAD(7KR<`UEiY8% zK`k^v?eb0Szy(yeoBbgIAh33 z#Nm=N%^>6CSlB`{<~x;~6f${Gs!3jjZiOjf2IPzaP&oa}Bz;NzP1-Dj!3xENBX1cR zLdC~sBjVRI5_(yvbs0h#g*B}$BHGdvuS?!Olgd)|g80RdzS2UE93IMm_j-yj;!=~2 z)XASs&M88(rQ|M6QZTs^D0R=K63sx3|(`6Nc5nu(t$_fE|2pyRVLsJ$Q zzczWNel!KXbjvxZ1~85FwjXcwF`h`kwPw{=tBZ-|BD3k~Z#kj@o%w7|&B>CLE;T_m z)5Jhmdr|8?b=DG}EMfvMlSrty@Db<8&2FE0>m3*)DcW64a~SeEG_l$D+>~6W9uQar z`x!+rv$Gx8cZ8f}6|I2NiR0lC5(d3o>>CX=C@Syh&>(9&gKCRW*fjy=TC3q2S?zmB z;4!Lqroo*8X%7aoG~PYc2o-fj0kXs!3Q}4Uk3qLhu_0irSh_68Ek;b7j{1?q)nLuI zhfcRw(M1W7D)E&WDd*vgDeU#qQzFdudvu~2n|Jv?Tvfv9e;wFgP%_P`-DAOLvk2PQ zqmYhLOr|v1g4W>;kYrOb>-OTtFZPPxuV9r0-cNepugMo+9uj)g+qj(7=b9VM@t)hq z$6|N>tKx_Zz`DO$Zq+g8(=#tp;{1NvD%}B6dS`?OG~ zQn>b!`YPbZ8cX`8{Kw@J@5kCnY*;sZ7(mquC*z>LEc^Fnd`qWSm8HUMR`_t`a9$rr z(^dIEJ`}rEnEIuj7R;TBW>^7{gnufJKCjna)Ws%^e+`a|I5?4PXBP}!4~XUOf!^vH z2r#x3Df@7SaE@t=7tE-i{}_74pKx&y@;zF3hns=KTu?o0VXht50ha+!nYSFM0-^Ka z=Kghhx1rhoc`vg)%R#|NI~7NTE5xp?tCFKn0RaE3LLk@6E$axb{2be`5y@+sX3%pW?1dQ+vtlU zUofR1*!QJl`Hz0oXBo>K934&#RBt1HK5e2Jy1=n{9CCjkwp?(h?Gc`=Tuc{U(T)|SScz(nND;P#{gEIS7%fN?DRa)rmr zTRd1e?IeaY*T>x=HAFE!pN2$28Y#aKSPVx|f=x_sL?RT^>wv7B((iZ8;f|j*KpB_0 z-=hR@8zOZ4Z8n>_g>6`BK^!#jJViD33g&ox(?4I8m@i!sET1COh?E;89$~S0>a}h7 zv7Lqf!&3=T>ZpO~+Ymf<0|HREL+>K}Y#5kqUd-=u1*e<$XYG!kZ$<~U3ej7Tj11Rd z4?z9By)&WOaPEJ5rIYJ(2A}%Y?dbjwBOHDXiNECie~XR(e~|yH(ec;8{y*9JfO}hF z_5r18A7vh3CK3jQ>FMk!Yx>F(?6+)0qw_)OpIaY(ey#DZ9xnbjE>Jcwv^M$e^WTP* z|9e>Q`$_vQ^1qFv-bWsHLJp(h2;rzV2M@ z8Dgt}k^+@Yz4LCJhfgp5|6}eeqq5G{zD2r2y1PR_y1ToP?(S}o7Ljh0?(XhxkZuqJ zX_PMC8)xR68AloCtaIjhKWzU?KHPilYyYm?SAd8;wjqMy4%JwHvSt@7-(w^eYgrdr zrpC;c8&qV4Wza`X9T!ombA4NW>#@+{?W;U?cuE(wY&g*v+WahSPNOoQ|02!;0a+g1 z{z_i`v&f?&;5-Ssh(vJ^c>@7`6(pIgavla5N8fP_?D#xtCI1wDqr^!Eg#B9&3>J~_ zg}@JQ*LZl5u{oiNDU;ZH`dP!gQpcA=Vx62`TRH*H=n;rs^{uI)rLb3Vpu8_6Dq;CZ z1lz$M-*=SK;mLe5<8&gfN+2?n)G7VhS2#uZh!BR{l0ZaJ)>>n|NcxrG7P;uAqAfPg zv}Hl;%9G^PuTb6N1oEc__=^}1mR&qpQ1&~h{>=+}7^(jYv-Df|-FK_}HuL7s@DnxE zH88Y$*fahE2=9BU@xlb2yF(Q!_~C~PJXQheYo&?W`BXq1hmt@1(VF!)(*EiFRfd?GGOi`un0kj=GCRO_D(+VrMUS5 zi!M=HkQM=BEJRixSlGyFm7rlh;RZ6pVAhAsqadr!Iug4R7B75%Jr*SsrSV02Fx^}x z&t3i!;;G`^)ot{Y`B<**i@o4mm3ha^w6Qx77s)kdtlEz)#p3E}G6_uJFGe)La|t-b zfW@EpA);IyPSqXVm0{m7$g?T!CHqSeVIF_2(AI^$y(bxD2e^j&EUuK?)O2MVnr#L1whN1Yn3ooHl-a9aiuAF zAp(Eq9q`}?`|9#u-Ko?1eB-KRPo}sQpMMJ4ZA+?@fDSn&Lj4Q zTAI!o7#oY@X3T*0$SuK|UoE1O0DHDQQOIc=I=Oxk1*h^L)`@uHEfitH5ay1-|NVEHgGh$OQ{5R!89HMBJ7 z{lTv-D+&4y9c21Huxr3SlOJV3cnPEOsb*rdO*S0GsxRZ5NWMOdL!7*#_Y_ifMwy6Q z#*Zazux%$djFfLf-kZTG*V=oFT9^MzCDW_OsN?1jR#L;s&p&_0Gq5r;evRczG6@fN zR^Lua9Lf)ol@SwinSufCF&s!f?I$Q^FJeEKQ=>Z2O*~8RY)PayL)wYH;Uu);;PO@(xytgP%Uz55HiX5Pw5G& zlAkKp+?gg4anC#V_h>f6hhC&FF_hCa@z_gnGwUZ8B3C(b2V z*w9`tv#gx&U(>f7UU2iiG?*R8-YAO1c_*k|CSXvzQ1~uhGgUo{CPFlf09&9urC@Nh zd!Nk)tZ{hh+Us-ripS(r#UQvq4bg2J&fE#xj2Hv8O*G#Yaw1koUOaAo0+TRaj94qe zzDK|&JRDu3ZqCT|6&}GEa+|ZSw~8w)cOWmhGxg1})2KaO z*-+fB2|I1UIz|V@20hAHVAien_ZN+F@?Y&(Di2V#TjhIe&{pS<5b0~h(+$MHRghZD zKmCf7?WIm1MKrBpBADYRfvtPp&I!icmfHUzZRN|$TpnAty*Jd4yJtEtg zQKa$OxsX=+)TZ&Uv~A)~+?W@As?U7Fep z7mF9KWYdN~7fi6+hNDbU&9Rn}%5W?;G1@*FyaI@6bvG&GGE{ z?Ko{6chm~?XtCCMxz3i)N|9LC_l$(6#VDtr*P7_&Jk4|#e$_`B!A^C0ansM2@A-Q9 zWAV1pVSe2*biT%meLpU%8ZFV_%W zAb$_ez6~Z(mXl+GMu-L8bUtKEM~+vKlVL*;{YCS2#-eA`Gx+|dV;hP6L2g6Z1__^T zCy19j+Wlv&KH!E7!SBfk+&N|8sAPw2ARBM~Wh%_dou z6VPyXA}M(cUHytSWmDy2=zNLxs5)T`l`2YJiPVPn`*{=|(vs(Mh9#cQcPa`j<}SW= z7UvY31fv14sNWxoWxqx0gHNKrhb*o3-vsT$4Ew+Ee!e~1_nhzFAL;%;e>w+SzpUm?3szQjy7R%>XRZ<5@>fU9pX9j zv@Usm0UhaBwmIz9spCO!k;SRX-L2R6Eqwok28*ha_4R({c(%-!KuoUcW-Z+^Q$n2l z*5<1hrCdiVDW@ILz;iW$lHTcnEFMh0aZq%mgvd7|!~w?(A-3d4NN4cKad-0-0 zGPlm5xX9C-uhhnOUdM{uwFS0#DS-<4U9NH^o-1JF2yzK=AUs9WV9%0uQ6WE{eb*lA z9MsNZ*8NTxtTJhQvWnFp>park-0~^!494hb)QnV3P)h4tO$9+rFvn1~L;)qXg<+26 z!VqVvvT39Ikwkxs5RHPxM258J9%r6_r5AVJPoO;G${E8wl^q%1@nflmcb@_#l@7Y| zN^+Qa)G7t%S!Ks+FJrpqhmKT>>DhcFQ)8{^LQ~zTId*jlvQ^l%Fu~gTN=_ixw?efo4>Dx9o4^oNG(ZS@qRJ!j0^xOJ3mA?J$pQY0MHs7Pt zZ#(?+R3ZCzpa1M>DynwpO5@AX!>QN?@{WvJ^p!0 zeajix4^c|W^}AI1Ii9<3=J+#Il5+hXg?`)PpQn&-%i15X^n2g;Pa;EB*UrJp(9Yh( z)aJXSdUB?3^4t11puROre&50A4_g4H3mBOi)BW;!*eg)l(>GB7tN{68ap13BK_FK( zcx5%AC|}l;o1CnM8>P_1k_D_)bktD6ONd`vU+xI7_Lo!_=h=%gfBrn|v0{N;$qL5G z%&x+CaG(_4PLG^GoQdNYIaVoME}u4}qu&1Mz2oIslZPvKX8OX%gYMK@mvf(M#hee& z`iRm}`Ha2;COuiSTT;+22E3@8#uawM?wgI&F`Z1gEJ6O!`~-+O9sT1|Y}owIw#hop zufmnzta~LVKK3FT@ZH+>n~?oH@(Q#m zT@;f&p7{pRo02!W1o8;-K9eQrK)F|6j&-Y))q%SLZC@-#=)+u?I8S`Gi+nZVu&~)eiu+0j@)B}+ zbYbhxrL}|?j-Lz8krmR5!*DmSsV!7p1+#o3M(@~?z*{Cq4FoclwA6-~n3Za~gdb?o z^f_`LHlL&-td?vDVo@rLyfabd;ZqdrrIzy-$E%SG;)H!4b~0M-0n%{}JVWc_96H+Wm9Tfoi>n4t>stk1tF~!OpzTqiCw&;I*Y9`4O5NWKvX#D63E0g3Kwl zEAG`)5JD^?H9Cs@iIu58r9?x07>tM9^D>%O_o z6lfkuDXg+%4U>KwYqAB8_t}j8)YKrPNv!BotP!Z7VJ#A)O(2jzSDynLg!8S`Fw*=LnUdY zm7?8zfpNJ3U~^OW6;(Fw?axHgR!YXDo%=u~#2?ecH!qTOs}N$T7xEJu6#3g>AY){Z zj-d-srQmcL1I-`kCnk${@C{9cr6{Xas~L~!Ev2>KhoK2C`ZFfLcff{i5n4Sv=|&I< zC8R21T)@;UwFP9uTjbWC`kAZCRK09Wzx3Ot&*?yIZm#lOfbU2HQ z|D1#y$s}`l0*9WTFeiuT{T2k9$+b_6cvkdb>z?nOe)S4lPON6qj1R7gX*$S>%&W=g zixPC$^jE@!bnYYddQ;ln;>@ACP~F)UP|4#jzRdL#GOW+^_OnV#t*AB_s$InDE|m4& z8htT^JxHuN1Si--dVY;ePSOM zTQ)YI%Va)vyYT=&P$OYli;q)ci|y{SNQz`d9Xh>K8=Jv9k(89`C1w4Pe{CN{=V!IBQ?TgD!px8$8n1cdPJ=aGIrfT`r{l)VY`5O>t! z+SQ4zM9(K^%P|ZL%!AMq%l6$WKHr2%Bhj3E26JuU$_enY#qLr2WK1wh_8I=}_(n!? zxk8b4z~IO&cVHA1Rnt4G^fS0}Kkg%=J1-Oo)^-18 zhs+~!`14_9d_>s*0!Z)^PKnDLSIILV3=cLzfiM3c?T{Lc^bFf2w!YONQ*QN_hciCrC9Pj& zAhGc&c__fMr}{&R%(u$myWahegdwf=&x*r;P7RcN&5+SdEy;(^z(_=&{jwxcGsdm) z_$v)YbD$Uh+Ja#EkxTMB3*z1d1q}Z-w9_}V0d$A@u?=;v5KI9^#QjQD{3TjqIX zp*@xrmS``Y?QN>v%@!Aq^vuc>)9^Sln`-|?9-c!l z&(`Z>tQi0HG#=J%9&VlN%!DoR=Po5siD@X}VAUUqJL|;*K2)z&tLq_T@#}%v2KH2q z!{`pRA}^|rf~`DG+vM( zChkK=19@QuMc8h4%{kig1Cmp)@wonWu2ziUf{D5b#OYmZcpX~(trP2o@4QNDQCvsW zb(CY$b5ov89z~k8TX3(r5$KIj8qP7*!bzeFxywa6PVF(n1PvkEN)82KWyzsbn1$rm zPe~vt%!p4jROvbw%sYugaVxb+00sHjshQTlce18Kh$|pqpK7I*CEEpKu8#_g&N5#e z%BW{E8U{_$ccD+!s_QARXeJGd)mKPhjFk-}a`|NPh%+ z|3i}fx59<@FFo?#aTuWX{_pbGKPP1vz8agCwbn7f<6)98dtVMno}1v&{xUB!d@0-J zucgd=GU9i`jlZLTwDmm=?pth|+8G+i>N=P_ENH5a^-Zb(%Q5n67vRShbxRtPhss1#_4F)?vo1WBkwubLyY{ah7_BX{79`0rkUZ4ICMNrRt-3%vRajvVgw z`DNPK%T=s<2jcNTyoVIP^$z-j>-{ah1^yns|C8JO=OA9=AvyAhLS1*82Kdj4v@hBi z@xKFcAq6RGSKWI%!qiIE!V&QA!w{E1)ik+Z#mbiEfO#2rCEB*zWLWZz&|$u+(1tYB z<`u|fml~#L)%J;$t`kc9%8y|kCE`GKsv#B7*jOX1c17DInuql-g<4d;=$;G3k@U*V zpm`rvA8>84je3r})SxxrWN6#Z1~u+XamIwTSgH27)BH4To0`{|JSZVSb(8B>pzCLL zGz{huFoV-;)rO``JL0R~SuHbA`8L+Lbl6KBsRy%V#X;jSmA&&*u{VO)ukhmAMZqoe z^1yaELFU;*oWS;M=;ql$&pvZ=xWiVRBdczSG+dMIR{7utb$a8PI|6zgDAK(9tOJz* zXQsJqLg zKG2%i1!{~Kv^hyenGoBzHVdS7o*L`>NkMyCBkj%VW5#EZJ(^zm z0Fr^mc~a;(4fXM}4I#U;j85HHj`_04P57HPdyiLI)1jV{6*2Da%YZ`~j5l}0#e=Ri_0eb!sycYIeH%pjSeOY6L*2?boTm5Lh5h@|XQ zk@z9}2{Q5v@Q@clMQ)~k(C10B{YlS>EKUV*t+dDs6yMX}SL{s6OXY~Go6T94SNh!9 zu~Tls7v&y$Y#`#y>L%lYW+=Xj9q(9fvAPxk*oksu#mTTBo!6P&dTfl+M)Gh5Cxngtc(Hnq~+@Vkc^joU--&0r5 zxwz`EBW;%`d^VlEmTV<8QlrG5M6^`$x=j2Ss_&ERfaM}#Mkut(tQ@cR7nKbLRQmh| zs_w98s>-((mS$+Blsa>O-UY3`;y_5-v@*ryo(Cv>g?(c-X^%W)kjSm>egC z@7vyXch&DVz`ZT$KDWS5+{P?Xl0{i#nnDbflCG*XeDj5ZU}_4TqFS4Y>$HN()vG#` z^Yv@h0If;7{x9SndTdVEs8azDlEFAa-do;Ibd ztxO;EUhZE()B)1=ex(As+Ab)gw~Usuu2V+hBQ6XhLrP(qS8EpSb`@w@yt5dBR7L0b zEF!0*1XaivoMko^MY10iRxsxiA_n$&nD{KnF8oX9vb{x+J_iE3cGvNx%iAr_k!xG1 z0LgTpF<=X8%nw&8fhF^SiM7mAQNv^h{bv&snQW)KBo`Y*FG=GpoBZP561t722ccZ` z2n2?I*nPZI{C3OQHdRG2rrR?lLPH%TAjI0kL)eejOy0X7<7m(C zUcz0zhl*?s7j>L*=CZ5XWLYpM^))$lcvf_A@aXb6@PzrhX6}8f2W9b`vZ~%+Nui@FKT1rQ zLNI-nKvhk2C4)6mG_oHfbqWcB7If3!4hPmXs#Hk5Ffh-u$I)7VF<9Y5bg;dr{XxwK zx|Eg+D~sA1WaH&_`}?4ZX4ES?OIx=w1Cj-YGDVmmv0D>VF%q@Pu5#u~-emWx=Yycw zDTc5oF!=^F7F%Oa<^&6>O}5#U@e4z@89T??%&qpUVq7Mdq0&G;5J0zjs`m(0p`+rzbc<00|WWm2{L6ijiG>rJ4`N!9q^7E(~g( zl-W=E(MJcG|Zlf}-g5t#htHINg(X%9G6-|EcH#v%@xta(sBfofYrWh633pZytm zMF8SEGHxU`T)bCJA+Lpdd%)fpTNUH$eO!fPo5g|U`r;@+F5_H4&hu771(^bd2@)w& zVo71X@N>r}H&I$Dd}>DyQ=JxhWE%45$$bG;l3791LO-Ip6nw3O&LYAPv6Nww>R`}{ zS=BnmDY*Upm5et34CYUi!6_=L+4*?*6Gf2%HE30jFkcUh?H{34v}EWi=p9^fpn2Ip z&Ht!jtYDtpbb`+gMETJ>Vw^-oMU{`$s1y(sGc2o(L@`BRjFVPae8) z@w0lerti$Dd=?MhOQXTaj-mz+uhb}l6Kh5sV5&@z2o3T_(1O0?S zm_V0a(*&0zj7`&WvXVNs6J)g@qI2XYARd}@mo1z&lc9_zCXVk-Z9Vu!ct6O0YDg=ao+WmJ)l z71yY`AT6S!c)^Qa%n+a9p3>(j2_d?yD}LD386UUvb4Z>Bf4HN2DTPw<=Dg#MB~`pY zp{EN$a%sRAGj!JV3H}=Lz*fNLR~2`zAHF8Rs5eK!tpmbjcCf$KIp4}yp1*17|4niG z&*__tu6uoxBR=`m&jg?T56^8@9Qqx&TX;g|pX*s}k9 zPTRhUw)Hzv1n<#Y^qWc1e9g_Smw3F{u=29?8N~R+B?UFdIZVV(BaO7kiXRbN$v1u9 zm^iXQXhaq?0-vVycH@P}Oggr94wvA}NMn-+ygRnJMsR0gY)wqP+#i~MY8MD3n?A_g ztY+&e&6T=Tg{46g4^gU2_j;jHDWxEm;x&>qjjAe8c`eF`Q&$j^4Gy_9viDO`X{_!H zS|NL-$L;`YhWA!KYFQO^SiXjQCzlth2izTU-!YUR|}TrLZ_~%NF1Z&p{`zg*b0;PT9c^ z&;4dJkGDz`VQbje)mSbpiMCIqDPB5ukh`Rl)m4_eu2NZ=@yEK?RI2*8meD*JfE(Sq z!o{_6j17Xgw36ZWJ5B98Jr|IMTOXYb^M>LfpcP=6CdD;PL?>k{9DD0I5G9XlDKFiL zPiSG1$q6!DW^Fhqx)2#+U$y2|73$+CX;m+T`q9CX`R(&=$))v4-Gny92FW}`>&Q=- z2PEQ4497Y^uv#TU2|hgI71*p;`ev=9lJ(M^T9ZWm9tr+a8ZhIe9{G5wARu;8Gx0v- zMnvTW%)xSxxFY#5Rvrs8S@+mMf_QD%LJ2i8s8PtgNv1G0Wnz0q z2Kc?lrJ3m!AmkUHaF$H|-mEHt%A%S~Q_#Z_h&@>uc_4w$HMg)cfNt0>-_V#MI- z!q%d`H1NeKFQ*H9cEeCM#Wzqzu5B0juA*uiZEC}1NUV%>-#2`c+bD%mD2S-2R+E`L z9%j$S;bO+e1G|yW#19J_2UmLb6*()D$E_9|N=9L*lUN-7Vo`60U#^x^T~2?ca?P_R z#Um(zlZ;@FJ0;nnFv@ZA&&J1tYmug-Z_u#k^y{`RzT~(QLG$maMbC}|&+GIFUpag^ z5(6*83EkI(n0A}}3}rjxw*2I^8~(xvb+oQb74k75ISZ@Q)#DrsTe>qPmfNvzIQA&) zg4|oO4I7U+kEeG7SGGtIm|||Jyp6ADkQ~XR(+2hyHL0od3fEFYQFA1wDE8gC zFtw9YE^Kb!rdX??M{TjV_A9>j{k+iID4TjK2e{ItXNrBfKs-|e45S`H#HA`b8+tCc@0e|@#Xop=xX>J2p?s0#RqTk96w!f63 zzX}mr?Y}2R9+sc~i$q_Ox9fjM+WDao{_Ba9><#UtP4&&KbS(}4N;rZb8k%tV8=BmI zhZTpz-|@l#5`HD6i@Qh->(fMo1=Qez_cNphvv+ivZIj20NQYRVQ3|^3i++7Vu$j1t zQ$!{p_z>Y@v^RQZ@mh1nPSYomk1OOwH8SLKL6|JZ2mPk3^-tmvhoiLXSZ`~)`&BdX zVb-jmukp0cS6cS)T3$riur+w>gukiJB$F<|B+H)wDODsD;46<(c62DN>8Ox35_@fa zVF6Uiiw^QaQ4`L9!euo&eA8?;4-^q>*B%+;kR*p+!7mC-mxj53t(Cc;`aLfXqIf5W zTgIsl?Iz<}{T5Q=Lj^xt2mMMc3Wfi-p z{8l?;5$L0bD;(89_Npc}r&KO0mcocwtYR^*`^uu*yxzH&ht!9;AZ11`c!iO!y*fTf zaP~vaAEQd@s;6c4mRMQS02{wd!J*y0+)Gi!U>^?hSBtmEp?=eA_+oNRf#tMpmt*FA zp5S`>A|2N5k#?JX!h&NtXI_^OI4_Z*x?kq8Kw#kV2h~1LOE6O!H*mDqK!#}@9lhpX zjO>&SQV`+^7skqhI`ZQ$-;S?=t5)g?G+WAqYlqtD`9J%FS78Q225vRn&O@#|Rd!ix zH4h=A>VpG*lQETV$ZDS~f*~W?&=-UsWc~8lQWqcn=cRLDF?Ttx&=tvd(;zfku)CXi zxJ|oz$>aqo3Dgi{3FT1ZG0RTVGWU_nQ)_=696*$E^$)XuzJ=#s+b{p(y#EBL5Axst zC%wnelcN}T3zTnl^gzbn9vs|x2+r9II9!^vu72TKw;D(Zfk)kNQ^aG_%Rg$}owC~c zGW2q}N3vAy6TPC|$BC@PCtQ3eETIM{XcElSuAe3Lr)OTXOs8nt$s645S$gf0?Zu5klxH$#ZiBh=+UEV9E?>0QBRF>^8 z@*&&#(q13tkt>s}JvPVG6&pJVJzxUq?55`v+X$N71$W_xq4dUinic2%LEMa1D#fp* zLQgQ^eOo>{>DwnrFX5ot48OGTd6Mp2eNh>Cq_G7Nn8-y}S?eF%0@0xdWoqKTnFc*X zCa6J87!|I%;v6;BVUoP1m*i7csaBMFRIQGu zXBpwjOpA|_E5-wD)JAoAyPo;`kkA)Dsdn+8A5gNmkO>;mt~yb&)j;o+k)=*Wx7?#Bnv&;6!`&Gsbn3aA|!(-Bh~b+?03g0S;9 z`YW~hw$rd)^d06b50R4*uED5r#(Ey=4-}MC57@z2^-Pi#_{0=%zw>()uGIX5d8*{X^$n-u zK9)XV#T)E7kbmAf(4V-RErb##9w}Mub}DwDO0(~G;vTy!Z~BB+o|2%4q6s9k z_s#d7c-NgLpz2ADMiDelr;`K-tL*iG>{!yx*?2O}=2uZ*v6IVNbSfb4i5kUuY^Ys0 zgKOXz{Wp_BZ$RV$J}N6Iw*d$Mn1NqBo1eI84S%&W1-I-_{@O?VVPfB}V(`A|;%DUF zZ(eG#V*9-j*1Sa(n1X4_qJ~0{rNJ7bX`A+#Y3W@7c&%-6S-)h0tqzr9$RY1yz1>>r z6{1UysdG(w8H7hd2+j4Ss|A^uA2H&3Y=LctIzLYG>&>W?VmHjKUFy2?+oC zgwpP_-kJT{(FMAdfG9l|dXFRxPKm%#{awr$$INFTR8s@kd5U({7==tAKm#>r@}V-m zI%+*UYlgK95r!{wR^#m!!cG7Z)Pl%tomQK8YDYJAeg zW*f&P2&}mG&K+S9TphVXr~0I*B<`ppk#sNU%N5_%c*SW3OYfw47XN0x zxjCoP0FR2znNMeCzk2ICLCB%v{j_>d>{h6c&e%)TSB!PZ&D+6IJ?AuyO`xzh@_EB) zY#ur5q*dg)lukNpT$l?U+hcl_!&;S@TOXkZ0tS{=kE=AD#D@e==2ybB^1U@8TZ<0_ zxxQ5UX=OtDd&KkXPFp*VYPry~K)RB-c7IH9Nlw&+kWz?+J}(qs#vtjOqc8Jq$Uh=M z$m)25o1z3RhgJsZ^|he5j%Yn%8K7Pj9;~$g>tv~40rpE_&(A{aVb1zrcrV{NW`D&n zU$YXv#&;eL>97L~hWnMR(0p$&ET1ByNgX55+sJ~f%7gH+KlAp9mE;Sobf`fe%;JAD zj~Q^@#9I^Q>Qm&Ib3!0KUP8S`>;{44#ZDTiLeTIu;=ezn2mTB3e+MHEBmTdsONzeg5@~D= z9f&_y(zjA{X#Dwo?OW#feI@<(jxKL#YH#oOQwrtkNZ;fVu(B1}@5^|buc^S^BITb_ zZW=+RBeaJ=A?Ip{!#{DyrgL-}KCr+~6ZZgLABlzbj(x4u?D$F(*=-xwV)SCaiKWsy zAlUzrYE;$qxI71G^sG$9Muc}riXmF=C#ese5o+lCIgh8^S#R!aFE00wZqZCYD-#n^ zLn?E{pu;w5#sk-4oeFs4)RzoLPH{7XV-~{M*33b?HccQUFMKI;w|n$?7%6ro!dSA( z_$vvt_OaM~UEf4kQ+Z069n?(-d$l`CX-?r_s$LJE4bD(ph{RjPzb1`)rI?J2svc_> z?a=oz`q3w-1Mz2lN>FON@ZuMywpzZ~4cG<(Wc@436L&I=dgZt3AY>W-L4h) zp!o4+c)6>Sk$t=x_@U zck&B-fw^-36&`)>;reRQepy^M?`ESMXGEkb+QA+_nt^-Z*OvsIf={o;0UY)EL5})v zwfpyAbsq!!Tfq9~n99pb6p$*EB#&R12IDw zH#GyJW4(U|}oM(NiN5S3FmTWFYVNZLOg@jM1iCW67>f5|_ zbR>|8OiW6oe-F79|pTP_DZ7)!teW%6L2p`YcD;_|mQBE?5+2H#oYu`W=Dz9eb zg7d4Xekufh!OvL=z96X*;n?r3+J&-92Z|M$-x{fPJT9=cuYVi6dUhoz-@H&WY}9a3bI?~(W`A&7#IpCaxn1OQq4J79Gp1G3AFEIQDundA0@T0aQ2I~$CJcae{|%69 zmhJ>tgty2dr0Bt*Nc^At3POSSvI|Ue3tl8^V7{A#JOj;>1@Dp=6Ior_=2%UT6_O&c z67?84x4#Ux?_q0`#hfs)$Qh86LrfZL$&0zEU07xfB|X?~Y~d42X!C$=+@Cq`y|%iB z3IC;r{&VsTIa!u-s`+Ud?jIlwn0?lSpjCilT$1 zVQQErZabDo!PvypcyT-=#+Z;~iMD5EdRLyKR(G%P+ZC1BaBkn5u1+{{WPNs%@%7=2 zGc3^)v&z32M`cfN)SE5CyN?IR!WI^Z160i)D?`7%%D)j0_z!ONK~w``zHY#Pc@yPv zzT~UuNJt>MzK)zg>xm}f32Ez(VPNF@FYDbxKtb8^-Pl69NwT~1DJVin@&|z0WvjJ) z>Y{zyvD0Z;s)-TvNXID`X1dAfDVWGwp0<`z)6*0ZHa1aHG*{#mWDt_o))f#>u|CPG z{~%xo)|W;knU92j^<*n$tXOEO{aE>^MyF*osMC_V{)%lF zjaaLJe$4zGR-~KvFe-_|g@$PSau)6NmyhtbGp|>gu2wuwmYwNehy?NAXy$nh@eFNY ze%@+Bi++sSC1JcozRGnyw{cTGP%?&NrhemT$A21sA+KtP?J}L)7^+R*%*p_$Pr&+japt#)2J`>{^qXFkwKKE_v}*dLg!MjVK__J8Xh|n!Xytf6 z)LzWW$oj|b*nK9~Z|m2A_D52~RIq@7_NPM6Vibs5QdbS1pcEWmEZUupi36H77 zB?rV{#c62!MyaHy1j!`_i^d-HqnX=H+5M6}e{WY z%tli3Gf&mnbBb8tIr0rK``MbYFJ7lBc(+^dqCj%U!f0!AuB(1UMcUa6HbljJ^%U7U zFagE6ko1dCz=tr~1{y>r@#X}++ihkaXVaNcPai)^c3iQx(v29(TXj_CGie zp!VBA*U?Vb%Hdw<{2csJW!uHO01rlhfa#lT-R>8A#SSVO16Jw9;4(-H4lE|oBvMm^ zpsfN;?x*Y8r$mwsb;VLbHgE)t*2_c5LG&Hy_!-%RvBVlAr*9fIi3XLxshf$zOHXPC zJ;XvXFczoByjrRc>GpTQA>b-8xO;PrOFjw|%|hp|@+)!@g+hiSY%0;u1C2sBWMySV z96+p0aF5PCF{7T2hztos=6VKV&>j>zF)7 zo9_Tl(mV=2WtGB#+(DJA- z>o3kjqp2f=8ZPF&8LBhtRVK~i9oue_x1C5^6VU5QI^|xUyFysr;4NP0R;dYxZ%V&t z`KlP$MbPjl01yNQ1o(ff7{05_e{f~@uH)akwEue~KCJj;?W`TF^{p-brTl&lhuUh^J-p*JGLK~M7nGpYG2RiP!pPIH@J{U~Aci#Rt z%f>CjDk~oVgrx_;@ZEZbAJOf%%&5Odx&M1i+^2s1*slEz6TbRSCE>?n1q0beZvUJGb|aEv zSmnjp!5D~FRogAHhyBh+#KRwq7}J+?-ACEZ@4UCX>p}@BpxW%KJJhBO*tKU~_9$eg z#ZAO&7WZZD>G#;zzarUJ3lEnw7*@nkfiAlR8#{dO3hR!G&TbILsAhQCQej7j6DO{I znXPL#tLmL$D^_4rnY85{!ljo|+>s=wvp|BfEY4e)WjCg#rWt_E!sNNduFCK!%sh}g zdR3jjdf+o7eesafo#KI5)KyZUTfnm9YN##wP&H?h9%(`)6MUONqe^ao(8mw4dP<`! z(w!3mF7$;}C=vlw1kFz!1yDs`b>V?5Rc7_s96RAL_JO=u-@uqhv`5O+=aVq(6XJD9 zEjCfp<&;bz&9}%yvuiFoKbgwOPn&@Pt?c)k3z-3L@}R>+)v*p9X_V4D(O7ehjttUz zIYEH{zT)p3_gIfC^%A4pm2$_6nvo~-VjNAHhK}Os9oaJkvXw8L z4eujaLmJ?dGQD+{=&APnk!QBr+UW#?9 zscIH!6OX9DP+u*rw-J0ydk)MFf&;F6(02DpBBd6H$NdDgq1t6HV}FdRP5Mc4OBs#s z+Z=0OE1@*^6C7{gd%KR}@mp|sfK3cyZvgaR-urL9b)?J$ z8Tud#NjGPK{W&ZPfqr=V=jhM31o;`!Eo|yyXmBroev$~kd7%MOTrwTJfSO{jP%`NV z{9>Krr**;TVDEjTd(y``HTX-jG%~v)BHDO6G9uIDf@xb|;7nu*BI!18R@zb1Pf2QO zm$*i6->lr)XOIUI@?zoI07~R@ibU4eEf7k4ydubv z6*QkzbeL--Eyp>LsiT5ovUg);(NSk~4rRXF?fF%nWhCfeChgJ=Mkapf$XeDZ1}96u zf^76MFkTAibAC?2z@lg%1|_0Fe8XQ-C(7Ld8z|QDv^$`@QH0w{W`~KpVbyt<_o#r! zWy<|RDSE}>()Fz~nD1oWx`dQVLuOC!JD=1yWizXSoBnu8aZQ38hj`WkPVY9Ff!wXi zbloR*zU=NK`!sEkjBywi%i+@*0e4<2XieQ^yuIfYdb6YMSbzZ<`Je&%H`UaCf|7?> z<$v)I{w@B0U)}H{6|87#X?Xu&^RRCE4fVenEd4`Lj$+helmCecYVS#;|Dc&t#{@N{ z0mT~;t)3zK7@1O(nzCFTf|g=nNJ1pi2YUL~KnuJ7`|TirDROU;{Mga>Zj$zoXhEy} z^VIlfZx?XJN8d*GANF?dr_TWF(%)>5e@)u?ZlA^b)F5WSy8r&0mfvpp-BxRVrsdb2 zf3IHtwfFZrRsOd1KTA)DM-YFa=Y3e@$41@PQ+-Xf`WH>_|MX+F`7Pb*{`*Qn&idU` z1h3K3G7$Lu*+r|SG;z@wt1vUk;GE6dBn<|}8QPk66aujqIKj?Zu4~$-A~w>=({qIW zC=_Q)m)8d^9Ou_^t)sK<&0Q4g;MqW%+|W&1XIdL2PFj8vQJ47_31j6oVX%IkVKkIA zx1a5=cdyTSt#eQ~@KRd$;I+nkJxz$?s4TIQWl91#Qmh<6hq>j-pgiLxE#N{LUpyyLz!36ovfOGg%Fdh9%aFT^v(@^*?;oLK|GU*9B=O-Z;T9Wj3 z+aim`4~L$3+K0}a_hR)bOUH|*I`O(I1SW^kQahdgC=mL}UX z_!8JFev~(3UlfYS;A<=)gj_Pwv?nsLQ^mMl57TAQI!fBIYu}1fN2(JegFnxAh~ao< z8yF#$ev_$`#NMt|#>l&jNPHeV1%_b!#OO=ylF_&C_ zY*ha}*r-amTL^byhapW38AM_)R~U8BCwrg-{ASuD)Wfv3r9+-{%P*%$5a!3Trl{if zM!x-Rqqf9FAp;ZKOWT0D8#uJGD6b1w|21gJ#u0RpIa6O~;YLM|2dZnL?CKe}sVARh zST-6&w(-g8NM3aOccXF>_Apz}_q(AkJ;t&S^m=qyf6zyX69cT+=VFW)zTO*iH;;-y z>%0*E7eP^RAOf`?@Esv*s(2>+r{wjIk#4om#OjATEh3z3LkhAFXaJjw-;9rr7g)H+ z$&7!}eNCb0kP6I&J2$C16~Bwd+&7)z)C*gOBGrlM2->;CJ#D!gLX&3xw4-vMV46*zije@aVuNKriLKqbl*PfCFp=o+V5QX{xGoCJHkoX@&QM z7T@uQ&2xs zaj^&O{qqfX_S%V)Re1LFij@15sNbaXBxDOA+=op*=Vty&*aw@6-(Z_WRd<@$;2Y$) z(p1Y2fTil5O}Gg3#QOWCUbmv9oe4VN6Ide07wLDX>S^~Ln5ZeZJsmhXM`y<8V^2qK zXJCc$gldOfeItrTbWW6VWd{ahgzI02_-$xd!rgBkvphJRZhraAS9`tfCr@9)U;0mm zRY<*`U?Q!Jo1m=5VK$*)fTq?4$QZhV{CX|v$y+?AmRM8F3N`m;W~2<1CQ65dHnCTH zzo4fd;xRx;0mrKlofVuxioza|6mw9yBXV%G#d{AW1FzgD*Xt|gsi@DuXi~N%R}~v` zwsFeoWf-LzM7sEg^(4LA%U5I2i+Kqc?OP+#jmmqR4}>09_| zsqJWQV`=&Op^Ng!eQyp2Ia_wIHyy%2a+O38styR!Mp*%C2YR z5jPZ;p4Rp4^_nP&>Aktk_^`DDf3A;Zbk@kAAU7pbCl}gI)kNabURCpX^V4Q1t~&x8 zkC)`Q7p;hC$)_{Ks?_NECe(g{a;#{IQeA)Od;&Uf#1%5j7EWQfkasp_6`E<;B?})c z(>NvFvwQm;`y7mf;uN2!d5Tb2cbH_hi|7sTH;ni+qMVQc2msLWr#gx9zm5_Aaz1GO zn*IGJ$LIguwMyoJ``k^o=E$gmEXY7lJ8|7z}M0K43>mYvIG-Q?h zmnxg%bE^mEniqt9;bfpzL3~(ofOfmms7P~&vPgH$r zG8-$0rkxb5BDSX@r~9}Q7G_Z=@Hd8iI*xqmWnOJu-b%C(ENfn59^P!S`xSR^BfK!tJf7GtuK8 zP6!);B{23Yo6y!avGOx+C>tBsxSJ0uE+b@UL?JRwelbR1xmIT?e%ku7DYoRsi*5nC zeS&%IU|G4rbb4;Z64D6@qg$OBBbpz&QDrFWp+9lXf8|u;gS}Tx?^+mV2$h>Jc&GgM z5@DSr*}-6>O78FV76npGX+?arIQgA-^?;~c8(&2!<0Ss)Z4vuW8j4!KN)BiiE(E8) zF~BP||D_}1`pVQMV)J}pRd}So4XQmS-Js0!s}ByudXj%P&m-<8?oE4)#~ECh&tuz1 zC8FmOD@A_sJN_bZr?Le)8gmd$3*9;rw5XnrxY3k|7$Mvx;zvZ*PR&cm=K9x&H=hl{ z_e;L(#kU*LiGMX6w>q1Gs|G&-TyEYp+^?;o1`Z1W#QwO7lF5A2-FuzzFfk5p6?MnV+Nsb2@&=BC zg??p*BHZ&l_~JLdW6yY`;2-0HC?L>o?b<}O3vG#uBP{^e1p@wv1tP{j4$}mS=bff?~r7H|Sap&IliVl*w z`EAq7xWz)t=Zp;y7q(3SMR&P(l~MEOGWzv9BET);Bm2hYK$DIXH~l#3N^NQSmqfNk zR;0kM&-Q3*J9_-tKv)(U#RA7ivT_v;a_4=xf|Kk{L4Ii6wpSxKzG?3b>ph{<=sL^2 zz}ttSFEN+#TkauTUW9<;yvVhrzw*B?JP<8jmkLw|J%xmJ1IG~-WUi`-E3jxr&Y0F) zlGKJTCRpkT4ET&qcLWXn7P3RSr@v)Q8d1{<$zpY*dC#j zUz%;vsHkF-sGv9|LlM-=D0kLx4bJ$P2qv#Kj>@6&8)|SE+@Q-FKX^1FS6ICBhB{Wa z9?>fNC#7w?5*ATY&9xWtcS!0WqwsJ*^6lhbpNYpUso*I0s^r(NQ5@$1m$J+D3(YJ5 zw@|5TcjeupOGxaLJ?eysDM@uCcT!?kxM2|MF2VAhf9+Ejn#-BfG*FSa@6qn0O0`I= zZkF5=6CsYn5J;C-RV+05Rni_0pIgf>7xO_7$puD(r?q_ZIt)IQWtjrflVewqRw3>U zxZKrN(aa3xF<}yh`G~-Mt!bA376q9O^gXTqnzvyS^5G$F8XwbP#8TB*LFd}iy^c68 z41~PAc(K#X(@~s0VVn#r-#(FSeu+C!d4a^|g}JNJ#lL$KfTj`nC?fzcmaB?7072}f zgmQpxeW$xdmNXt~S=mW8HBqgTvs~5+kdyJPp#-Br8T-(Y3CSytPV_ZO;pHm~r_ko@ zIT$h#(mwe}GyFQI2TE=>7_0D|YD9t5U=gSxEz{{%M7)ojfj*QV5eEt+v(^lD?fsnO zgMlg$X=qmFfu90&x0D)6yK{F)9S^I{I-((LnolLeyWVm-qn$;k>2kS*A6 z5|F8iR~@Xml2vyMyf^eB0Q4lG%lDQHA#!^1==7WB%0cL6G}xnXV<;c?pOVcTk6V>?GFm&2=M1 zBieL;)Mk|~pfTV1dyucQjl{k2%&rj=^hO}Rkb9=-_kTrtRT=T3Pcli>;rI5QfwATZ z4EGr524CEK7qxlQvo)YlSe0tMjd?Bg~Y6kKweNar^nJ!F>VnqOr4L#Dx>Ad{$5Wt~^ zBo=S+R{(c_Ha`EXY0;}Kl3o-X=?yq#mtLN)GX*>2)F-Sak9ewS+2mC`uXTyvsz5dHG>lxlPt)$hY#z2bPn`KU#H1v(td(v!TwUi4ElP zL9we+IpXVS`$-mT=OiO8BAFsZ@`;hIL$N(-FYUuQI8RZ0+%~HTv{b;vwS&B#OyNTP zbft+KThxuX;bL5qcU>+si~WkvpJTFbFPYAYxm&4)s(?a*fCupn zmt3=)qYbITmTY!(jOe|j%wPx!XokUdx_VLGFjwLP2lK}DOzq!-LEuE*Cw8_As%P(H z@{z`$^(_Zb1hhqS=~sOA5AV!We;u)fPu4|`n^Ow(;}#`_|2UV6(O2>}XekV&%HLNa zMeb8qWgixF(k53Y!9O4r%U?i^h!+MdM3|%;&x6X4oR_T^ifa8OY9$>>i;jk7tu5g; zf(GwAc=w7Z;%E)c(RYC1_xjX7H(p?5+iGBtZ!_l9+Xn_3b+p0N*ZQ<>tDLKuR7j)% zmEn04jhx>WZ(AKVevX9{vC8(I*QXpPHKyo*VG>Q|8*VJ<+AIsQBfEuB?0g zhDnjhMH@eEO@lJaV^xl2Su~ZvRWNPR^BLQ#5UzZ0PUn6}TD@MJGV}*fmPhqY@!Md0UYGu==^Y%uE-tW1eeNKX|X8NY8`=0RFFG}fUx77D=M0glW|AOghNHOqEhqki`Jgi0^Tck)SL?0vd* zx~mQj(gDse_Com~Wg=%a@`Eve&2Jx%Gi2&Leeq5^SfjKY&bWdorhdhhB z<0L`(k*!0wO@WnxW;!xIv4Ry9~@=5PQOtlO1Z= zCaR^gzX|5e=9~w%t>8BHy9YrV5)m;ieVZ6Fq1IAA1%|V8sG6ObS=?G~`0{n?v|_)s zLil@E4;dwYexGPD^dfx!mR|ZK(ITfHOeuk@3 z2{L4vhnL%Di~I4i;S@g3c70g6e$YEn-0h}+xaNGPlw#5sq4m{=Bou!qVKiusAB{q% zPh$%czd4p{(&1Fna})vv-6x2MsP&r*uGP>fT)@Uk4VV|wm4E2}R&y6!H8oveVdt1v zm#WYh#-c^kK4I-SV4h43>-wJnZlN%TNGBrKj{bw zut-hBdForfPNfaX9mi?M!tvs=L?6;d zVI|c#i@2aQ?oO!UDN|ah2Nor>6Hii76i*gOV3MK_phRLyUb6x6&fE%?dI;Rpx5P5Elz*SrGSaUSIoeKb48C8#+B^+j}gB)$&F zQV@rLY@z83+5%Xooy2t(vK$zaoi4R>|I)fWPAo4Kydq+3?>?q)B0>Jcdbu$Xh$}GvxLMF~?V&Kd4kYvNU+4{mkkBx4z`3 zg;Wz-<^D5`nx4UM?KxqP9>@Z;h&}^GIpJ^<0@{gwvr6Zwl;5QmjYclbj7j+;w!7#Q z92-OYA*06kE?MIdhmrof7i{UJWsb8mhzk?``cw3*cm~cjld=O!F^I=Orl_J~)(tL9 z*y-Kb83%u&uo=^6@7LWcmu;_?eK;RJN+FFijko(rlCJ!-nbSzSuqp6uS+y=01*9e*xF z&Qrvq5ea?^6ERUF7RqFw;8?6_(H>QK&J zgFU&1um%%2=N;pyHE8!>RfNtVG2t25TZ5q*x)@nsPw8q1)FtBPtRE^_;`I>IaJ-oS zWjd_!G799K)+KoW$#6eE>Ja_E1$!W-^S11N9k~nmjxop9Xs@$%b z2S&vVRub^abH_FW*kpQmyJpz0@{n|9>6962Sc%F8hDcr^_kRLY`uYV=F5rn~%@wXY z8h6I8(fUq?9Ii{3;7A<rdhyVT)?EvHr{?8-K zw=UdrJBmouk^5lcQR-9>hb!!I`zag)vG!6F0lsVtK@%2NA8A8YKSn<;Q98aPj_M-<(=(Np)KFUkXd zl`d?Lkn0U61%WL?$Vpw7)+ul1N25}}(`%m}^Vtt6+qYOzQtl0qpV`;F6@_AelS|X& zPMRW7V#_Zt%P930r`%9;Ozy|ht|gnHUell&uMUFzv=o)8zc9?Am^D$(apCwCIeuQQ zm^?l|27eywow{F)UZ>U;=qn2mhQ8Skal;YLk)8sac&ITa{4AB0DH1bgB26M1lI>ES zq-T*fYBbO@5t%8Iwx+2dLIu<)bbVY-KAOwZH+NAI9v&ml@Ps1hH4&~X!mjg272O5Q zjsb%BbAWJ$VN^r$M}RFiAS^mwOqq4I8??|y?X}|GYLnEMu%7^|LtKFlXip3Q1bk!l z-q~f5$Zl8u*3Gf1^^}>J(_jE1`53xRlPE&%ShC}(iQ0w_HrEG5YbqW^asfY_nNXd6 zxDH-G^WatCmhAv$(hhoBE3F>ISD`x_klAqolb>|AnEOl8++-Tf-Rv%vf15+6e8=&m z3c?FO6qR#$$$9McW#GZbGh=^1mdnQp4ew!KRs`572U0@G5W@z|do1iu4|>n|W<`xq zb~~0dfIq8B6f^Wb1LH^6ao39v553TE&FeVQ1@I_F=|qo1XS;3Y>%Az@Q3_jS-$(Sx7Whrale+W7L?cN)ix$vF3eNd&@V` zeSk5)X*&fw88>AiSpKb?o;K*Xu|TC{IA9;uBFWH=rz!K|@ag7#8AV~Xo$8n!4GR}H zyZR8|X~`-w9L;(;$ccW$=mim~wR07D2?LDcjLD42vjzH=~!M8W*5%q)1Yr(-Pi zgN+f346~{RuyUG7l#G#emA>)xpq1n?*Ap)g>Vm50tQTx;7wNK?4vpWV0GOaBM$mCi`E1J8_zW&Xuc^&l$WR|pQc|ki&()+VE>YW(rQ%my zYvUd zSom*SIBDj)w1nMr5RK!UbXf+Rr?!MlzBOQTu1VH1y)R0sz5nvHy?4aUY)cPm@H!hMAY6}H z-*060p^4(*O)8*qWC|FdqV2A;WU~~MBl9c@2&@(Oh0ZhwtS2DU-U$P>4endH1$y91T-R9O0~n%j@vYbW4B zJrA3!FkElnGq*~mc!EwRX~s?`q0HmZOu{Vn92fMoMsT(O9u&93oMphWpv2MvmcipK z0mCV6DENfJ_dbBNF_?V6Wl}sDC$W>(N!NIBhk#|l0wVJhOwSGKtiC6}*hg73FiPMk zMjz?E1b3?QZ4$uK7BZvdKC_@>W8U{>2|JDP0G~j~Q*40grHLBY$!1(W*ms$f>Qoqz zNmX{l!Zqas(e4cuUF_xPXtmP`j(tQob(vqE^5-MnoT|Sb%H#^2Fk}NKA|+^P3F6V^ zs&=EQ<}H)rJ#2q&lNqCz>vyukh4UzV?knN0^NY{R4H{HHOheO11aabHgfH@sM#%4W zqdsA#EOU;$Swm4@U3>CY^4AEwNO$c7`f>lN?8FH-p&=#Afvdzd0~iHe=3oD%D~MU? zqZB!33GRHsu$YIigUW*%29!S*eXQ%oZ3zF`V)|!B(@z;h6=4T$~(s|?oZH_)$*-)~R zq=65x-%7B($IK%ckN^PI1b+>a{|Fc5{=*XNe~cXe93lU2HZ`!i{?^oB-?{3@%SZs+ z5B>4tPV%!O{rCE`C0HxU-)}wsBZuJocd+zt#NoHjyMF~n|8>c=NO{tFpBcXEjBMrS zH{f>1^iB{klpq5WmLcMcZ%yksP)Oey>%-&=#pSm!A^^Oi2e&*fifocXPGq|iiApww zA@t2ydA2>VU`;q-hYa|w6`^=+f6*$;nG=DSnsZ+dphPTBP^X-wyuJ{5qr|6y8RHXY z0K2`u-0kQ1aC$#Skcv@H(P&3a)7-?cFZ!0G%_Uc&UbR?tZP~(L8tam$v8v*vx$jD2 ztG;z5Qr<;XmrxAQg7-JmLm)&q5h6^a0-hk+2VJ()-_qgz!u!>J`qmEVCbngfvw?W$3b};Xa%_d(o>k@?#OhGsrQ*420kJ+%F>%01S8rD3J=2Qj_)_Hr z6&7gc1m$|6q`Swj^a*(Bb^1}=QIqZ3gaE)w$H0*DH zhw*CxOIUYe_ULh37?Wjnm~dE5pk1=R%#UI?D7FDdflfkCC8X-vF}taC>R*N9J6(Ux@DW2;PHG z8_q5X4PbjRF&~O@H8M>0q*G}>U^ggk?)@jqZ^<-ajF!r1Xuw0V>shsrFds3T-KS3q z*KX82{S|70-;NH4^Ms98;=Ms7`&FYlM=@^WSRr@o=S8w2)sy6YQJLr$^#P^nK}z0$ zQ2=*N()|kkdGU=>eTHYTNut^3rQYhNqf1XS$9iCEi=|UJ@CoW~cc3)79QB;fH}U80?e9vSE==9l!``rYA?b)7AZbSUjTa^M{1 zz(f!`E}5Z`2{2J*jb#mv!Hrs=HNM&t=~H%9SVM!as*zFJIGb^cr;Mf_zh@ho#|(t+ z&rX>@6ENdTB|2@wppdY7704m(G8|-}_peO1lD2 zF?`0NPB+g-t^xcF@pDc=k+#Bt z81&KW+qrIzyL>{W-m3XUcAf^6>W{28JXHkLMsU*SeYKx=StlU|1L*42)IQ%;-$33i zh1Byqxls`=m}wN!s;hTk&F*?#SM^(Jq7#fPMZ`Or>VexZ33IVmevFh zee23K^r?@qPJ#FA`4yFPG0x-&ruDT$Ca{o@^t6DBGWLKPhjGW8sjzF-%}_zONJ)&$ zpBm1q`H-}X!!zX3PdGd_@pjK8qjYK)Hj*~Y^g2w?D%K9?!m?6duO?XD9!w_86L%ZK zl!%KO%_wUFX6lT>CV_WPR+qS8*bUu#B=gcIen);N^cVbBFTTgb7kN1k^m2egO{$K4 zf8(5E1>4T2vZc<%;z2J>q-5>lqV7EEWa_ zlf+$B&P;yxGu%P1BdWA@lf8g^N`~t>`91?&}N$vji?R5JP^ zoLJBH$+5r$iDDRK?W)YlC{Z?7aoD) zX9~?rJL(!cS2sFTnZGec>6JvNZf{oV(1RfDuACxZOioTVUewD4N66~Wve3v}cHwlo zmbE{1wikrd$U6Qs{&`|1F5Kzw-dKFZw1wG8Ry9Nedoj3U4w94Em z#jm-io&F9xG@VH8T>>(i~^P7`NVHiW?2 zN8%25Ee;vbw{gpG8bzg(Ky z7I2KJ!FI|sSY6)t%3k!i_&|Od1;)WDkV(KqM!82Gr^m#g-)USpAt_Int(f<$S-;XT zDjGb6u7n>TR*9R}f~9vFkWj2la44oEGBapoP4s1upYukF8G*-ix~_JaSbuz+9vndL zM3{V)dLfr<-Ehv-PcW$p2jzZ&>%etw0Unx%!O1d|qxI-mfv$S!14B{8sqE5 z!6_or+dJ4=&gu{642oz0f8VAunZusqUS>OikaGY$a4+bFI0jV%@*Zp0`9?NpOb#6v z5XEdv;ASk-kdo)cT0sI3R@lrb#kTLsNKYouG_k2kK%$undM>l{FEQ%V)&Ecv^4 z2_7P(?4GpluUXw*6QYSOI1XXk=HrtG zm#DSPAq77(HRn2LyAwS*YN9jbG_-8y#{(k@t{j9k2Y&YWcss;H1U=Yo(B)k*lMy znUSU8=X{it-NNO8yu8>Ea?!Gx+9UwiX)fm*Lo*-SnH8rIP+(g$K3=*~0mZn2KMmT4LL0Y)a% z&S2|K5#ET6(Ho&67HBl|$*xN;eT*!=$xXt?P1E@3esOb>Vpo$6%LP@10pM`r+#XtE zYJruM#+V;k5aC)fn|RQq)^)r5f|xFxRS(0)?1<~R%$t;VU2L&louDbTrvMN41AQHl zPv_{TZVW$3l+H@7=fE$A)rM5`Zo|Y8sv31hjr^l$h()M6RXzz2%s_!KHvQ~pZn8P( z15gv`y2qZD#Kt~KKbfjVl1-4xkB=2eAB<&HMHss)wAxk*nP~tQ>VhBJzvNdFl9p7% z8E2x9Y|Sk&GD7T2sCmK%n4iFvW4K1-pGhhWtlq?3%uc^G z&%Qhr?nX6r{1^h$y#QJuZv-|p(g~)r3^ot%hSh1n6Qi1x&UIN{zvXq4!wuV9c+Yy< z*#B8yeRv!Be3cO6GRVzM*va=Mk^m+h0$S*ixf%$5RVzwkkKmb}>T29y?EJf@=_xrn zXDc88fa>3vT>SAE{nl;$=gxV5IZxC&|DzN2|L%H`6DoY}h*+|(0d7t7i}Ta_n=KJB zECc0#f4z{Z|F#SLA39F`{&D<1ZzyPQV=HLmV*T4M|G3w@G(_LD>@$uo4NWgp*R9u? z(YhWL;8&>&Cyg>0&%l7QQrN{z9HjzCPic{a_#_f1l2Ekb!_QD!(H;=a2+lW`TvlY{ zP15iLBKy?l4M4uP@mNv6POYRlv@XnQ?$#g-e4|E>uwUtgb_ zS`=6ii&$hn7U>t!S`ZTe=>Nsh)!X&mG7=}3!B?&fUCG*lXchS+xH?o&a=r+oK*cYa zQcAo1&2mc3v{?v4R$71ycm>TZJK*W0H|C^BH;%`QK~cD3#Jp%FQ{gAtU?9aBzcAaS zKR`Jv|C7NS`_Lh@6sRgeQJHmSphDD9+LG?}u&;+cE?1}8EM1>@0k2255N{!pMMMsj zn0}D^1ZPwP(R5sPJW=IE;1_oyq420Fj(3onH0{iJ#{40|X(2%_$zSP~3!qxntfDt> zOX$XBH+gD?*9T_BY_-nH*vwhG0W@;nWMc-Fk9-Cm+tx-qT9!|PbLHVYvShDNRa44A zQkjAt7I@zHm~mz`b?#{9V2;-JYp2ww?U7;-)&!1wLAK6p!X>>6oQjRnVZ^U!=agsA zA%)1!HwsfW#Opd^0mAeYN0V zA&21!2T6oS_2SSL5=38Y)y1IanDoe!wkj}IG+Bcu?Wp=?bPoe5(YJRP#Fz#UpB1+gY2j&2TQ*&_G3O``p$ML*OSA zC`fK@aN)FBPn&K`W>f_GJr$JGAihnIwj3#36y`xa`l?lD4>W`IoFGG7Nzq$rL^S`i zuAda-{6zbrNT92)Q07IqnYnosh2G=o%LJ0Wqfi1|_^&qvH(!zD?RJ$Heaiv7;gOQ4f7-bJz}h7 z)dJ!l@2t6aJg^m!j>7@%_XBdMLnU`WiXD7oDD5M=y>ggbvwt{NH6jgK^g-6`E@OF z%21t&B!c58qU}yYVGtzzR9$<*?;VMPFE-2x(ZwC zqzhp#=B(o>c*W#jqc7ejl-wDiPEDmFyvpPE@p|BF)uU2 z+%&a$6jg$)337c;3@j3w<1c9SU;d(DiU**_F1v!COBZ1m=rgf3IFS7JeqU z1M`f6{R;4b^Q493mSZ8`c?Hrwrp;S%T*r~c7{y&{SY|TpKq~=O^?>PUODp(N5T{ep zg|ngVEB@lhl&p+CP<`+)wQBNEWG(B}+mOVr!8ajy=UvjlLRxgD_8}p9^DdVQ*7+kN ze;hO{8ZY~dT**j4e0>!YWZP=sDt#Vff9v3*_gy>S?I8H6g?Eb^M`!(m3cUYyw+r{6 z>-sze_&89EsaF3{So^tbc-iWBXmFxEqIeVu_nK@y07r zPLWNmW0iZk1x7BN>R7QWbb3&Ia;~RSm@oP; zVViPs>Dkt%_c&u^Miou;&%GJdP}EXfcYk~7|$lMTJHo^qywl=h=O3bqRbe20U3%;Omq zmN=ktoxRH4n$qsHlg)|rXCI)yA@cGHsqCarM1Fz%*NFU&Z{j~6#{bLXNUihlJ&%71 z4n7T|lL0>sqwPR}<$CA{2_gD`PIV0d>63HG^n$~`(9(($9UuQ7LP0tx{BcN#l9Gs0 zQd~%wTu2TMA}|RLh>!&heFWk3X>JzA%M_-(+C|MBCRWCF6Z8~h6lSJ|yn1LPcA6xl zgpADAI;cccHk1a>a0CQt4F)C`hz=_BWK`xB<~CXey>}soj;HjM=M04JXUULhW2W1g zL&Mq+M<(32HGR^BN@SPm1&SDj?-Ew^@9qKHjSN<}N}}A!bk;6W4qDCSV)IX#Vx97H zoF%IbZNQQ$9#`G>@B7&6X^~TrYeJM6P#Lh_l`H62&y$hf^cuvX6J{^f&R=@gAsb{E ze}yb>bKdw^kDy+;_`wZW&12|T)2{TIx{V5@PxGD7{;}F4xHx(w@(KP*KjR-e(ELL~ zqrZHQk~Su0{{-Ru#u(f`8N+WenA97cK?@&z#Tf+01#f0iAJ!&MaNkp`gnZGIBtG}> z1EgOaAIChSQdy~xd${~j+hetPOyFD>2#_Cku}KZL$JN`o3q7Xy4r`j=07Dy?cjipTHQ-CS1a_S`A3A-8CZqxv zXnfKAvCBIngt1L4N}Xe-sAzJmP~Z10g~$=2c~9oiOfV)2A+!%EkFEpq-o?bu6q)y<`-U z(=ZD|_|@EGHw?uuvDNy)1RVLnS3Suk?LSrYbDg?lLP{w_>&J6?1~9`ZtqIuN<3szh z(p?nha7C0cF4*;O6@Z*Lh=ovDNb|vX?TqegZ-{O~2{4jCn_&+GPoP>P`gih)Nf8Lz zg#$sg13xd&H0YdE(8^Z%ZwG{92mX}2!r!KlaNL?z_Ko#IH(Q4;=dBw}5y_{2t@hfl zPDx3$yQD8Y9DVP^DRV9&m)~+M3Lb1Xc8N_B)<@=9q%WdqIxvV`BPG60P@X^B7s{c& zmKa`S8S6HoOw$i1Q#m`z+a`l4#3m;vv~LOMwf*ReZqjP{fH+`o$Oc}rU4(SBo%^6u z{MxoR#~D^?{PppMN9BC!1=^*ya{Z~|PI@AM;jJUJ=0(K$QS^nI;j-3GLz88S;J1WR z@$HuY;iscY9K@fU&wuVV^_PQ6t@Cdj)j!QP|Eufj%73RKKMuXi{QiEV_~%;sQmzzXq|bxnCj~z zrT(mCwao0g)UQa5BlKSX7E=T8`z@}kx5n+4eRxpr^r}g3cC8ovwudq*A&T#qV3N7_ zLonnED)Ym6@13m8U_VeHO02r&&E#-BMs8@2g;IW;U>q5C;&0EoAcMfMIyhM03u8;= zA)^EaLW;W*T|%L60+Gz37?bV=ib-*smKPN3F4z=MnCgFvi)7~fffoaD0Fo`TW(ep= z1+wmKc?vbhL&X;aE>0Ys^C`t0ZSR-7ASQ%D%^f;1=0p?Y8|O=S&LRO4IF~1?L@a3% zxncy6fKT`6Zm)H|EQ-@XoWa-v_5BEtu6B>fd3Aj~ak@GrSO~~b0sovc5qV`C8+Q$G zb(%rw_9YNYJR}(G_|A0ET>hs0WcB4%cBB^NLJH|X-H+z*=Ku}gJi?{C&9JtfApG{= zL?gPH2#)a$K*YQjME^ulmlAwrY7S`>=3^xzp?Yi?t}v9a#m+7QQMQIigw(xUdg+Wi zBOArfS^;*yV+3W9dN*|Y-ddC6$V4o~ACc1HzRzT=s6eI!ci4ND+7W>eo~I_a%J%jI z7E^?uHRO&6E*k`+mebDn!(Wpej<@Hzk9%)|+pUHjQcw9Yy``BJUpz9AI%N)o+q}U& z76$#n-lh83tYaW0-4i($P+_>{g7Is?fRzE3v5o~->|(sKcC(5Dxb&NMFXXF;Q%rh> z!e&+#@EJ9{U0q0`39b^f6m(4(^g8Z?pGk#cEx6J%gV6iEGg>B14Zd^{DV5sY($$2H zW=>pJlFz=pm66+6zQ)?yj&8*}Lgz7uUc+-hg6lL{44RVBA=5o&#Hn4~%x~@IfYG5_ zH90WF;n7{`7=R2!0i5dceijEbm?dwEemBOw^A^~INF+&bU64$uuF4E?<5XPvUU~} zb_}nETU?<)J%+eMN=|I$Cc|zxbl6UVd7~(wfh0?G0LHwBEw#P{y+uB4@(_n%SU_|H z0jWLcv|JCkSqXfMH5JkiL$ybLVU?!*ugm=YgFSz0+!amDysYHC83EtP+WNSeq>;2_s$Y z%9c@>b8Ouq(O_Ufdrf&-MfF4>YyrGVLI<-^a9p6KPj?g11OA+d+R3+>%RMQ2I7lRD zfuIp4wI@&c1|j&hgLMcG3%^0Th>6}OrddxeAPXf9)5e}uraQMr)&+tU(Bp<(DahHX zRB?b(`-w_nV-I$DiB|6Rn!_?{5crO2`(L*qPZ?<-I-FLqOzTnOvFxz^%W@verLV*FRkWG3jUNaI_7Bo6j^{ zugP1R^JKcKzbLMk<+9&vB=@>#ZR2jD%Qu{lOj89`Mq9BL6o{>^44mz>@@Fxqcx(XN zF{0+ZfRH8{ZABY^_q#;_D-OEQmS*%3p@^vhwsvL0r!ZY)n17mp#;YK8#K*7)+nD>A zXMITQ^Y8c@9;QOB&k9^!|w^psN5CG zGE!0&-cU}>nURHeyRpdHvJ~L$AuWI`r6siN-!ordMY#KlmP19hub*^Hd+oPXIrk)2 z8=44EnMf(?(nWB^j?mJR7|c5Fk`I@JcIr}#Dkl5rm0+U-oHb*Oy2O^Mi#8HsSE{-* z(72iBKQa#8S3#6{k18iIPaUJZDSM6tk%v1SD0k@`iaV~>rwK!3EV)2xMw!=j(*?qNp!sCeLaol8DlN=FQ&dIau_Q!e1 zb^(lWvhT2$alR4^1=SD;8o(h`Deqwk12-rw;gU8IpV30Q znY570T{5rLTRVL?HVwylQ6id_p(l=A&o0t+Y}&Pc4kD{dxR-m6y8gV_z4Z(g&f7FI zg^3e|o?N`#!M}TBnHXtbF_(|)5_@VuP?~C%L#4LUH(|;Q0#{w*?)EAIJenWPhJ`>& zfxyh&5XoNaITlK08*@q8v(h4C=8cb4{5Wh$+k;4xD{LwM8P~oQrWQY0YzGhtmkto5 zsRUS*#@u9k9In;xqQPvG2Sjm#nP6r(E; z7)VAW)N2aA8y>YLixT;@TtcSs8rnP&pRm#AGLxD|FRA%WhSteh%llG6Ix+-ko91K+ z9AnDOQw_;9q|WDiX2;ub`X_Gn0nu~36}h8`jFrp6gQ!$)&_8ISzMObTej&09X@U@f zCSV71MJ|~=dK}CX&fq~neVpk(13MxThPz+;aA4x&nAnHy%>hk3c9l+$IACjryB7_|pqKsg=jEu;Ljp0yps&0A#C=q*O z>{b;}o0L06F#&K!MW5uCG>ooNhDyN$(0wsb5p_W-+hF)FwB5|d(#=P=e>Wdy} zKFAm9`YtID_8ufnizPEd7iFe1=ml61gXxS^hYS^Fq_29 z<~>aU=hB1uF8cMrgRrySu#WWFVkE|$#Ry7bd!9B9lt4!;J`DJE@UF3nHHNMsMZrn= z*kq2D*e=3~uS?VXJZtN@or)MeVz!N6`ASZ27p^K8#H;K9HcR)N@|E@Mnh;Qj%_5kDAdhj zbCXo}vYjf9W9B~=B4c6H7LuvbY3THu)1Wb9_vP1FE_`u)Vw#LjO0eI%aicb4ndCs55T>GL#>y45>Z zw|n1sh#r@#_NIF@@xGd6WLo6((Fds#edi8{x_Ou^+c;+JTJ705+!tR2jU0y%{QOLn z{Q%+)%m*4yU!4KUezl5o#BTM?5J?ti*;@fjRTPL_*Y`~L z_uqH`7q(nVO2NPgWBL=rKlv3W-_~4o7;fYb zyKDF2rZ{{BF9>@wajB3m(z;cWg8*Jv(pi1miOLova|RM#mFe<;daeVM|*S1I3>wJ8>$3`ux z6_P{wff`E?-MI=Ru0990H;Cwtl^KQ66czoku_s#m*W@oKc|a9Ou{5{U(A2wRQ441N zyBU=cwq_>W97;{!V2E3E^I$3vgjW1uits>(eNeD1A++4c>(M27 zwJ4r(>>{WQ`K~^yKG`-TT1V7-kiSvqp3=%g2DbT~7>r*i&;@soI(cy+P+{C0LEn*M z%2-O@Q(rGr&vik*NuMQD84e+KU@j4nQKC&S?p{_Lt9L@zS(;9Aqbko{K3CTtFZi7R zYx~yBJ`@#OML#l3?5h}*Bs35HBYNQ=zZ(O?Of>H05AdU+ySTy}TGpD5Dq23LN8C86 z?3@H}7!73Ut+ES;@ba)9X(tr)^6N*XUu$wpS%}0xb>ZoN$i9b0>8z!$yzf24QZD9E z10E85d67f0n5{%A%1sT^rL1)SGED-DpUajV>0(7I7ojLB>Q@Y;-Ie|m#xUTe@X9yt@1ne6$$`hRI zI}NW?(T;~12*sLAO+cM+gf>JHWn8HlhZj>W#+N6s#h;O~*_KcXPT4;1t-6czjf9Qr3r`|kjv zze2bFUpPN_ex73FE4>?=1uT2pXPK<-Uq( zbibX(eYw%ZcUy%ww^*_ERSM3@$@BvB)vNKQ?InCYlXrYXCiJM)f%3(DI9PLk#pQjd z-KdCWpjGtwo+0r^K{-)BVW!SC*Rlor)M4pO@wt9a#ycWw{=1Eio9z41j;b!~yYBU# z-SkL2W*JF4GCBsj$;0|QXYT$i^lwXzayhU1i$?VKM+l9rzh{K~HT3$A%aHzIh5z3S z?Z2|J4`&~A=pXVcRk^F7X}i*g;x%8DLjD1DQ7B>L>eYzl=i{cWz6PpD;7gcmAYvhv{SBXGXX{u zJFufQ#_qVM)xlUdJ&iOy4Ut)7z3jP6YFRe}!*#^=lktVQ4dr4jp3RH}O}BQOc~h}o z&4rJ;en!-G(K4acn(29u9ZMdq{iW_u)r}w~vBTtiMCw!wB-3_>m`2mCkxBQ{yHyxjr-3GvoU>(uq@=)>3of;TG+; zg<9PGS5JBEnAO=@MpD$0e6?^PoP|A zHPFQDF?Y=DTq_s#yDdS5+`IRJ3;aNY2u%(<6=)N01q@8AiH$7px=5}OEw^a+Q8_(N z&pG|hs=mwIvcOk|+&BQLD3(AWzD3Ll;#bh7OcBR!-2{<#nhuMg?UeWhS1dw<8x0vI zeys-Q+uAAOi83wz$r6s9ewU5nkjm zt7E2KmBI|grS!Yh8SBN4Pv^f zs4Z-P5%tyH(&|-7`-KsH%(I(InDT28usjexASC^wL?R32Vh7lRs-f&`@=;BLFp<^r zeZjh!M=aw`RniCq6ZX;9Xk_{+*T{)n+n4Pz3WqwoP`d4u#S1{cK`2}I$eB5(Yk>7$K<~EQRF=){k$MUbhsiw^r*_*>@-v16K$2;e z_tmCd5S@@%lI5VS0t)~@k7zIJMEv89hNd!3%1S79hD(EU3ao(H4;gAOzAhjG@XaKP zXw~0SG)rn14E=WCg;sq!#5vgu{8dd zkTJNF@0hIR#+!)Y1@2iNEGqM&*wG~Lqz5vS%z(bcjV~>yL4vcJ7ru#4=I8!4#eSUn z!>vr5Qvo(ev=x?RkgJei#xgFc)Xf!=)e8^yOsEYQxr^6Bb3`&o)Bj+P6y0-8J}wGt zMD>ahr62Iy5@rBaHdm)&X2U278n(AfY6_Llu||E+*bUajIoBm%>7x(}m>q7BEo&CY zkqKkO&2wHYd-d5t{`2Ih$K=_q*}h0)f*H8Vb?@#49JK~?6>hgeH16r4O3p+C37tn4 zIAG)~h?I(n;^i^M36RYtadL`sKjmB|SK4BGCFqlclhUR-e!n~AtO$)~8RgAVD=bnMdXs%kmU6Q02Z z{p38)F;4i5t$x%%;QW9>-R~^#EA89X#I^OAs-iYFAy5O1=S#6NxdfhD?UkzYcc+_s z+p72N3&l%T73Ff=g%@zqXj~$9p$(#n}vH zPH{)7S>NdEcsn7508!L7gVh`nsv?iG$JZ0KLw`Zf$!EMIeVy@*wp|( zf^MSkUPMpRz5()i72qSZ!K%1$O|S;Xh6o%>3$VO60|>Fge(v)o?16*TKA9-AqMj(r zusRN61Y5+62AW%X$@-H{JZ4v`%zmoPdFz5`Mes9tGsCJ3N4PaLOP(=1#)hCR2G}#p zMw2}3Fz{ynGGZ} zB~GVcjjplYNZ}*|N0UC8n5zuiH_6`{eC7t4Eaw0_HBCicb+oE~r+SRVjBB;?gKytVH|z&TpqO`^%h%45zacw_*~U`T8+ z?)tm)(4MppWO7pE$2A_s)N4>_E{8J5yCeIlM@%iLFKJAcbf~+rVd;o}{5Z~UO4}bB zLx|U~GKCKBRF=6Q)QRv+w06C^Z0&+0l^f2_OsnMqrXy7uWp(70yog^EgxtE%&?`_x z-ARstj4@P#DJd~(!O#%z8@2}hHd4kS=rR)V=ECdma-=`Hpl5Amrpz~z!TYJR12_;7 z$yZ##pnm8()KggZ4Md8ahBSE+xn}}IoWT9XH8{O(kg4!tKm0iX+uWVcW~9Qoc!fVm zo{-`ZXmQc#Zbsy@C5vUk&mkDPF0EKL@bW|ujV-}KM?Zn)qZtC5gngA<9v2Hj2IYE> z)WiaSF-LV3x@|sdE|Zf4AMEhn)>3C~t&m)x7>ILRQl)D({vFe#ELX6YTmsshi_hx; zy5=dSPW^!*>?88U(G2i0pL#7#^@*WTc(Ls~f&?(+uEwShAuRYGd}<-7!!Z^_!8d0| ziD`Z!n$R~(zHTUmWyF6M;qcL_dm3mQDkXf(`efjrLV@F<`VeXWV?KE8ebvuCe(hvP zfr^tR$kd|f3it%p|}0bAT?~-;mm$2Ve%=`3b48RtbHt#N{#q%< z5X&Hwn$hzf@C})1)ZYN;6kyu3=pNzsA3Q22>Qme4Q)|wbT!WVerX7()*|=fA}P1i^doJWO1hi;GQoLtiwzmf+epe3 zq&94v!33H{YLAz@Zc4qeFk({2_-Z%G@6|6i;EAeHnMP-8=fvI8w%JX(GP73kIkpH| zuW-r%IxJdM`w9X@rY}Y*3Qy0==q`r7?eX{K5G64f%6H z2PQVRds?EBL(*1VJ*zAUctA_I6HgvJ+3OZLgDK7PBBfne zumU_IA59HsoJIz!_2xI)Y0}T_eJi_ykoQRDbU26(sYfJ$9PK}yr%wEIEL@_@tc;it zcGW2iMIW&_6{a&Wtzk|CHX@xbpr#fmnS|2lEXVzfVn!t=?sG&hJEt}!8p-}M?6dRu zkOi&Vlz62hi>#Tq37E7Dk0s)|2Ftg&lii5PM%B<^P7uf&MdI5=yMC7^g?k8813U~j zb)xvfV7JuHSX3~+inI$vkfX#agG1i!J2G~jFnXAGa<-<*+O=lJ?{_fkXx!*oBWJ~L zzZK&thK;fxLIVJ3)BSyn{f~i{^#5gy{eKmM{r@4ljvbHxh^`q36&F6L^T)1=!_UPRwZsrela)K%O%`gg`rB&rt{9NH)SV4+ zBodu#jFIBFEa=}cJjITLU+~4Z>85zwa;)$q9*=bpWv)4bG*)ohb7^}=CN7p9W7%G- zn=9dTJoJw~CaY|pj5B1lQO2i9?{~PPKX}g8R#35E}*RhfkEc%AlRG6M^ z^&OY6Nm1bgeIzbu?e762ecbFkOjrzvUv3z#u(Qzg!~5+HJ^2$Run30F`$%LqFoszv zje0@SL(BUMzZmV!Nr@oVL7Hpeap=LnD3k{vMpo8U57K|M z;=ng#wGsiUf#j438y4H(5suD9ske@=&`sH9H}6tJTw3Wa{H|^tkR>w!5xeereVwU( z;D-Wt#Rq{<$Y)a-qR`vkF*%Lh1AG1h7U2AX!T)=Ua9VXU9uf?9v~9W?&1N=7rIJ{M z?CBQ`1}e~NEi&EnuR#scWFzR8-(hZJ2ZHtJ+yzBLNC0sa=d_s`ADAwLuE8cZkJ!rcR&9$2QWqc0QBkI%So<&f`1YZ&;)_7zUtM?(k!|ma<*vPI-!71R z|0oLvTL}9nJi~+Kb3rZa)ruL=FxV3+`i!Xa&nE%e&xAvdTJh>U{O29`#AH>6!u2;{ zpN@1pr*Vi(H`8bs6Q?h7i3op|z^MW@$^Zu%2wAd?G;rp0$F!{Sm z#(y;AUo#5-g^cKL7EP=Be{0i!>Zbo!%NnIP;y)(M;Ii45`TcWQqjhNP+CP^y{$uCa zAM$NVyUFRB8vm_@@o(2s`Ub{Uj(^-^^{WCIuT}FmagLpo0Q#+Gkph;-Cw)?yAB{jb zsHlMFM`=Fvj?8J)5Tb7int={-qfwy&zQji0gdf%n)Moe zNK6%SsAo0S*O#EX!XzSkIT$?{!;n> ze#vQd|Gh>3&nf+mtFWmLvb{4g#Q#C*YavQU{#xn(a6|vEFYq@q@xR?Dn46k8i8~ov z|2d!VFXeamuKAl-%MMHQ@FB16Q_-qR`fU~YAxqP!kcUm$2pL&@JLF>^8muoA%M}Ab z_)1L;RDdq1tnBn{xU3)SI*#h&APUyUk1HU@uo zy<1;p#mU0m8GpO}ply&Ej|g_th8+M_!8p3}4~_M5zm!4|p<0`}>Tj{GAuDpNC>ngz%GwS2qMYJOnKv zi4+mcQf+JAnVV|B?34l#x`~{j+cy?qgPP>H-4W2u)KdYtSGmEjd@dS5>CXWiyWN#R z^_2Z^)EOUy1$e<`ma6u@0Rqxwff&&)c>4v zhusN%D7Sj*BBswjdvp(zBf7suGkgv@|M$b$o=cr6tEr& zF<`yPFkfS2z3UKMk@wKE#PxUPK@vL^Py0sMWLJtZwM^<2(5I;Yaw>9*m2)Vbw>z*4zAE_;ehFVrf%bP;7kJo_}h#z2hLm=ru;c09{G zAY;N7da3Dmeh(;0*36oDeztuEpU=n~3NQ{gi?5J91`rKP_rL)cY;hiw04cJ~C35R= zP1og9ppVzX=P6suG{>aMkwt-a>^qM+QJVrXSlCM`;;(Kg7Z2N{xr8NcSVdf?J#-l- zrm6sHqCY_k;+%iET-2=%*|cGvJB)_X!oVwbsU4fnQH1G75!I8(8wT2!*y&iRNQpw5 zYN%GI+p5s-sLO>M$J(O4Vi+kd?xx#b^xC7uDUNy%01jEaoyrlJ4A%JCZkewKDqFn0O4_;vH=NNVU~Gx;Tvid|73EZgn(RMcGrDCLSn z`h{X*)arb*XP+kPJ+k|@=QftQTr`BuNIHP+_lJz%OYv&r;akNuE6__UOkcQcU)GqS z!0ksCq?vmoyH9C6o7X8Z(ylxp)LD2Hvl%4PC63^MJ~b9EoW0GT-AAvE^NwJ$;7029 z=U;#MI%avmR{?bl^ue&Lv|yU*n>SdigJEI zVF`xg8wi83Qb>V6Gb!~eTQ3*%a_aGgv2ToT%sAR1@PGPK`IYG;RWO@Jief zb(2V`@u66Cu_TOu(QaN6F~?LS7y!kQWQ;z@B%i_WCP}=rX%={Xuw*ZP z$R7iRzuFaldw*p|V+U1pCo>}l{eQtie?h)q(ylh@@X^8bF-Vgx6?Y&tMJ0pj)~x7s zAuQg^fD?CE0ya|yj4&Co5B~>H4i3q-hi*<2?lVC9UFuZufgdu3A8*o$pc?`t_1*q5 zTzKH!J^1r;zz9=u^awSPuq`iLGt3Q5xHONmgHr=*=GX=IJ2q7g7$JjhtgZL^<$1QZ z=kvZRA*fr+``7mkVZB~3YVkq_BNn%Qsas|grqND*BgjWS_V8kTg^VfoixU3AonxO3 zBn22^B*PNq05c0l^u}NX*at^K=;E$bJ$Ocbc1ZTwR+Q-w?EC{OgZ23$x}3WFVOOGB zQQ8D4ehe`87}>(GRkduJVnPfH4%5ws9LR`)8FEt89nq_u!JHCf76QZ2%TMq{xgq`p zK#3Xoaa<-dbm=>`S?h5#i(0{9g{ZlWdMx|YBfBYLjE)^D9-!HymS9{%s!F|+sw+-)&SBc#2ijId9}yD8~vrjsULw08_yN_gZH ze}FLRRCe(wQBb17OV>TZd^$jQEKIADb={H0NzGzo73kcv^`eSK(nn{wfvUBTLl>Nv zuqFY0VGcjZXEc&U;4gPby-b(fDcyfgl*$xAw=JnmJC7n4V~5eFa04l|OaxOL>;Qs= zf}XuTp2o76?-QgjXdFPc3AsJch6*Oa8;$n z)38$KW z*u4Bn4Or)HHqy(nDKXR0Tn%FNxaK+4-yLsxD_?$+ze(*-5eQ*8ZmJBpbG@WyXDSQ6 zOJH&qu4hxlAc+*2nn6Z6I5I0Rs(~F>c3$%yAVo4Y^gxZ0tM7XSs+E1vg4V|8v!AC| z1U5R#C&7#ui_^@_+6=H{2#m!!+PauH{HiKAikuW?#(K+nXT)%?6}K^7CP|!Kk*hfR z;5F&DjD`A>^|)Zn22NZj%FfmD$vc>r~!Eu z4lH^VKu^n*Im4B1Z;BfAEnj%Bc}RLML{U?BLpE5~NI{Avsh?75lgVnDL~>Lq%cO@CaD8yJYRqw20I8%*pc3p`eY0yUO~m~~ zq0{7$S<|L)k&L^V`r*v70Ze?q(yUfAjoC}LjcUcEDd&f;Zb0_o9^ob9JFr)!*<+1& zOR3_u2VA)4Ng`HS3MxvIg$UF3H?CgrSW9mRhAxf~amXjVc~QF5RfZ}|uP&8Lcx25C z3t=VVV!P_2kgh0{UM7b~mWsUDhf?We#Q8EF&@LisId8OlZ_&KIAt<#VlM+-HoqhbR zKS=B)i|8jP0D$xF#xMM1!t7s0z5lhN{`-5%pYN*wi5aPX*z5dXfd02vlXI|jF?Te# z{TJpEjD6tPU$Uw+F1t*J+_7JEF+o^pCas{SsAwKs?}SJRA#}*rR~+zdye2u`Wq1Sw|gk$A%@(uYM61 zS3}WPPi6Q$%D8Tka%z>7+OVJxGO56S2?^PgH?pH6lt&jIHAi8Pl6`KewFF4nNHI1C zXAaUcZ?Y@KK2$>!hH^h#a7u+L5@ZvggyNbidpJTW9*$f`~J1;XtB z4$qELK>Hx>bqVWaQQBM~;9cy#A;Hz5k{k+bed7$5F_;_YnODAF7(V1II>e_9o7~!6&P_HeY|?4bCBj}6K9b&k9uls_d;oN|ba|3&J%ZU>5qDsBFW11y-7f&9 zBZl3JNINw-k{$&8hzvvICC2voa?ORvn=y8bDzV0tW=#km-3GEilPoY%s118bkf%-* z$7uQ+CE?R~H3*v-E)YjRUQ`%EHb(KinKF!4(MnO~YwS;4q4*A1vTEd!9!JJudSjMgY=-!4DuFSjnW(OGd*7I(?s3GRRSk?)kT!D<+;`9>!umFb$mQufrdV{M#Oo# zx}-_#uW?;7yyT9+HG+Kgo;u#Q95kf23cicDw4n_5~ABK7|Uc$>#x<)`wb z04v;E>#lUy(|B0ptLzFFb}^`#K7urtf-4_*aLe3obsSaU1?Jvfl+5b%t!Tc?eOBHv zu(`Dj;ha6)ojC~GC*#YNQkBh-v+Aa?M`86wJf;L~umutUQtb0!H$;oBx=cZ}5S=Yl z_GVNp!)|_2$b;XCV^c#bNN)O~F;ko0JbTz!Wzu~(d0Kn;nCawT)4>UdcIPX6ruN&D zy>H%heEK3qn3q@U8${LTa{`0MhtkC4#trMUeV3|+mJsiKYBi(&F;bSK*`E!GzC*{i zGYdy)X9*}2jERFXt1C6uGcZS-qocV)_-NOkoL~g&OT7O$4B4cAA{4s z?m7H>!=u&xH%;%)9m@aaM8&b+U^*oQ3livL1T-|`%lUEU#I*(3ZxM^xXRGS}r7&91 z!C3#}fYtGrBl`0&y8FZJ{E|YYE*m6z`1XCO@(dOTYz;t^Sonh6kV0hQshhBj)y1Dq zhTzNT#T5?x_{s@bS|w~sTiq}nVK+qR9`TW^i@+h=LRpvTmmb^uOz90K@i9?ACIwTX z*hNaRZU$QJRh|o{cXlm6{8CS3I~ds#6kr9{H^yQG`1k3Wnxm(jGXj=hnUaQLbehMd z5u8D>p6%D3=*@WGnW-(~o?f26J?`7?Jgr7Ql2le6&FikmKlWCw^U}My@w`6-s(CfP z(RH$A>uODEBrsG+o@g2+n;!(ix~UL7=%4%FmA#_21ZIed5&{Btn>8JJDbNXtN)(x| z<)OKRhj6LYFM@0(MMGnP+63mhlYe`HNZ3S>bhINkfR3SO5mPJ8Ev1d?#vhh5qdR?& zE*gdC(MGR@bWK$H0iq;d`ol=#3(m@z6@O_=i&@)@dWl`_Ok>f|o_V%#KQIX+f~!#) zQmBF+mJlfqXcIaW4?plO6RIj|)B-?~QJ9`T!o3SuMt=pLT$4b?d39qSg@Wq0!>bc) z^+|pDd|-@a9;NMGe$8kC2HA-&OOEi`TD5Vf+>M~RgzPQr7VfCfoK5kWMRa86L!lak zdI;g_k=fC!JW;FeFGEihAgnfVQpoyI3KuNfpLD;SUeSfbv=LTm?g%#-Ccl|OGE%Q9 zN|ffDjX7%JRLRs?X_XR4!2)6&1eRD&^9XDd zZPvX5vArCyL~*|!s52W@F7x482?OJ3Z+J}g_ma*$T|`W)^Odq6(7td%MF7^PCo4Mp zyk(;n-oKerH`ILE(1%+=`BS&@?*{+>o>l$lybA1ZUS-LpCK^wOk%4aHqO;0XV7v_D zw@;zV14#dSukw)x0Qg7LM*VU4^?z)T_u2j~pln^2gsPE5QJO z|BCfl{wwDHV_(kl@uWXLWB&U`{&!OJ*F*p8nScJV{}Y(-59j9p-Rt~ML&@(Fo&WRm zpZjWOZv!)@51B<@w!jv>)7F(w@jL)*G5!wcOVAiw$jqr$ixZj1g>ERZI?EX7-pZjo z%b1k{h*T?C{aOvh5|RV*`Pk~J8fwkB&IMn$!2G=@tMu%svO}C`mjVWJc6X*_wq>rb zZ>bMVpzWHQq0s1#;>Ef~%mFnhpv^!8xCHRcXVK@rUKrW_Ow>YVLx9i zN!-P86uy8Q>g@;B8yl0KbM((RK=AZXY`7s%)d+eruc~?c-Ua~e9fr#fYSvGq6npHW zIF!T_9@U)deTZd?f@IsWT|zUQ5f=df^n>P1Kw`-vN*wKAt&pK{oToX?h39Y+sQyrq zV#JQ^rcboz@+Cjg_HJ&XMKnyylXWYeKp=tV0RRXYsR0q29a;MwW9V@v?Qn32WARMl zBR29(P#(hFiqK4&v7|1en?)LR8O!5T*f%cKBYtxfOX(&xaTVH-n6)Lz4;WFft z-#5h?GYmLZzoevPlG4;TTWK2%6SRnP)wWV%Wd4`Ku(fH6vQKIODh z!D)l4CQ4)8ebf=rNsUKzp%(&4eB}jj$4E12@{n|z!e^d&cgiUM=1#l5GOLja~fK#Z!Dq8H@mvS^JuF@6-P z*N6)^ne2SMawKKo0Z|Ab|$RZQtq+7T(h#> zkIfog%kZoT9sJozz;2(%T~F$AlY>}^1C%Hb#AxL9t59M-vVJG=*Pciu`pVGhp{>?L zVC7QX#Fso160bcsQ}i(U{cH43t<@Ddx`wpmsyxAq~=nSQ9D&5kGY&`IPUP9kfPP3c4|b0=ArK z95pzrbTX9+P_0b=I8xSrjOFOYu`BbSRyo3OZqM!n-k=e#?#4YFfUI2 z&B5r^bN=pn)a2D8mYN71m)kwh>)mUx>bvy&o8;5%^G$kk@~4PprJri<+p>speEd+t z>EXTDYL%r3l^U1q{^%nng2kI+d*=4o3bNp%-*Z73LkPEgBhAS-!4d?C!h2x=-pjex zE{Fr)Vn!I@`D|hx>g=P2QzIeCC=vOY#d}Y;M)Fh*H9SEjih;J_J&dC1)u+$jHe@7E zT)zO<+S2Bz!l;G^VfeuDxA76)!cIKG*7A}=PbM@R47SK7HEhLC*%mIk;40_jHi(ge z2du-~@#XrPiPcs=imAGCJ3Y2O*&f!MZA3!o%Tx9M-5rpmMHR0PZ$dv_WYeUq*Si?(4(I*C%(&p42= zTTa-9w=q2M5?fbT@%IZEUW5)ytTkcH$DHDFE$;txqbG8aE0YU%9O{~&cea14hQ2d7 z{+))pRG;p1B7iZOVI^Q`23J1W#LJ$dci@d$Bbi?{Q(5)q6hJ8I=gH$}{5D#aPxz$bf6Qqx*nMGMRLN06ab@WEN`$ZJ$?6mKD zPeIkq9lV0pioiG!!fJJ%VM{CO!XRmt1b@aIb_3a7|C+Ag{|r}tfAaP30G2;TX8)VPwZze{YCl>?X+-dU4z4{c zp0j@sgCPJQ{s{&P{|hkqe_s9r#QmxP{Dn>b=f^*XaNLbLW?UaKs&Oj2B7*rorc1w? zCDVqO!t|{TPK;e*p+FlHqF>-Tw<51AYxQ=s0mlP+F`O_qKtz6H=0*dv5}YE|Y+6jL z$$TJD?2xzqW4P%3$bFXiOKWjDn*CI(<*Rg3S(Z+} zI6XKaSW<1)sptFmtV4lVo?aAH8pc!kKr`_bkoP~4){OYW*<);>cO`!c5! zf6S1RPrq&mNoHp7PYz|tUqTM?bTNg-uvC_+Nf2gX7&ugb@r9KZB;$ZdxlN`6p3Dzj zlx~Sj(-X0VXIhPIkW(>CR7)y_pRseosOT;(EgeBuqfiAB{unmR$FHz;D)tg5H^!n_ zE5S4cGPvpapaFd4UjA)3X;P;)RG9?k#B2tZ+;+dlQF^wXVw!k?Qdd+>b=hjzPa8&X zcux);J`QT+)18sl%uNt|xU(F3vjmi;P;6R&3}+H3CM@JoqIhWJ^kVa%xY891+><76 zVxG9+t7J}unHrl3p$0QSnO@Q`K8<6!RQ^H)K|1Cs`}b0o7GNEa&8ewEI(wSBez-uRH-bfFI4;TL1tB8dci)tEij=CVVip-u%Y|&PC~$S zGaPh^g}e^eDs5sM69ix1cLA9bb*|*xtfu_D3`VKl$^J0^0TObP`FkE8k-5lpla@Kl zlsxn!bd5y*z)#cz%)6FRE`>4(#^bOhc#@Y1wYRd2q=t#Fkb@Z1{w8}s{qIJYFXgl= zWDmnmlMlBAMjS4ddSV7w@_-m6yTlcVM3PyriUuJ8z_~NvlPAf$@Hb$a=Sl{9pLNKu z*?cT1$#d6(!o)3bO(s*Sy9s_rO(B;j^e{a*OHxc_Fkv4pIp* zl_HY#JM+5*(lS`6esfdjDUU}2GUNi-o(J!_2b`|2yn9s>sVrBWc`Volm;6zE&x0gA z?Iju_niL-%X?cWms+l4-n_=qfvzX3#ayDP;4}94hlQGrg#22zx=Gk3e~fV5=L|NU3o~j)okdnj3OwerxmF z0${EYRl&9_fFx9e*ayR8;g!ZmY>5G=Ks?)l7}?^9X>Xeu)t16sonE4!N{tSNBX_T0 zO-uACp#phVC<%t@oBsP9q`56rTeu+5a6S+k;$}{qBuYyTUHsr3?G9;%3!PP@0PQME zR|Xe%_!J`Gdap&OFFSr)Q`=_y8vj~5=c`BCz_RXr)=LaR@dYIUWtA|Ix+|nepgy(h z)fuk)I`@56wU0e?O8ECvn)`m%;`!I$rY1nw&wdR6SLkGC!})8I?$}h4H99{Fi3cv7 zKs7&=$qfmNjxb6u#3HGaKuCh^L6@=sc^fbafC($NLkb3&4=yiAQw_b21K&Rj+U^7V z;GlYdp-@`a(2ppI305`FBx!?O#MBCEZF-s00B(nzHBB&wIGh}S7?H-m4c3cWRaY=Ilgi z$^*s16?_)dS#*gU_^ft*~%Wlsfl=t zcc5c%NHvGe;8Vagt~ae4vM8;O0e{oqDu$=7sV zK(=_s1uDH{Xl05pvrvXG;?*2w6`V%p%=Pr7no8N$VehZ*TofzuJwOf)N1P%Bv}7(?SF) zUxf`I?zN><4yQN$#ce}jV%uv(~Cg!oo z2SyK{wKd4UT4y}0D+0@L7;x}Yse*G_n?MI+XW9%IB_l;nCY1~ zipV#Y{Ya-SB{BXYbm53}nHh^kawg2fFvUMfg$_T>zzw=dg!hhy=OjpO5K^tJoK?EM zo156DJEQny!kB$y!)y^52=n8_`%1@5r2u!xi6S6h>zG~lzRTO* zjx)!(s(z34O)6yk=E`(8V2UacsEZ-KV4Bjd-=h&W8m~_)Lp#w6X)lwq_CD|Ul=q9K zFe;EpX;1vo@y-xqLBC^nN?ba3T|`&A$IP)GXeW=rS#trNt4nmT$+K&Rax9CxKA-*;Kn13$CB%?s?mJES_LQ8#vgH z^mKnprCMZA>kq+VJLe@DzrTO4D=%@77AM7e2~YDuEd4Y)L7RDtC3*^>W2l>`#%h=+*ftYosIE*|^#J0;e*&mklj+v8kFB zKn!Yz(gL<{$KvygGzJ@uz*nR-)wj)Vo0#iG(;hMpFGZ*vTPb;^S`bT)a z^WOVSE|a5Xy6CMXz?%vhI@hgO0x9ptFF^uY5iVJA2;gNCXZ8gPqaT1 zy015mg&Ig%vR7EbUU7X^@-%uT6B-6-huc`>v!;if7s!{)pCm%QD@?CQA>5^4-O(Y- zlhosHb8PuHcYjI6FFEkB9Q|k^TTk^zu9w`uLVW+#pP>6&3jDAC{&P3uf6wul(U=-6@6Un6F-@&*Ri08MUJosEqV_#-5XvI0|L zil8;l?1E$7uth-f4gEEw2Txbe<>}A_8g18Jc!*)OM7(m=r;i2gxMMFPllT$b(cMAu zK1nWY^&2b{BF3SbBpaDxlPp(RI56p&rb>ZUdg4LODJRZDWH%>5s)G#uY6Wp7su(g? z!fWQl=RsV!8hW8ZI*x56=Ady}?54`#`TE`VvnO_ylU{u%_}a$obhA@Roh^ft*04h? zwI#}U4|=PWb)<)Ei$=(O>z6r8#vmrYjp5$7^o6C9ANMsuRLuT1Krd@YAz*9cxJaUf zb{CA%OY3(lu61*iM|rFbV(y2_U!?%+sVkFrIgckD-E^k0vi3xlZR}jI`pP5{9ur@) z8>jGJe$MX(-8@(3RNzw`YeqU$&C^E=4ND%aFAcChgZ<(?h>pIdjeo?URzIp6e?RK< z=UQ+7c&Gg)m*8L7YyW%aj}&s)$On1T{c#KMzsCQrqfU4JocQ=vC-i6WzrVgb-M>dd z{}tvV`s3*R8i`7O>DK7A{ZY;RS*QjHg^C$pIA6`OV$Mug0*`cc)WnV{erkfW2$9_Q z1=Y-)3Z<;juaGV<>^>LdC3xczkCW-ij*ZlJYJ6(q!w_e3otEOfD3}LO=dan@qaeAX zHkGj3CIcMhq@lplRo|F8a=pC;mDVC>-a)8zoZ=q&yrBg&we!{8gp`LX7BkV=^r`Mjc1(Kw&VBMdy~?Gs9cA_G$!z63HCo8o~u;qtrHzsLcf<)jSNw8ALfk3&TlGVl1J=R;pkWItP_J9sH3g zjl33$@S3x4{(h-Nd&Lqn#N*<54lTVviR7rVB5}&{AXkW+E~UUlQsOIM^*{-CiWBr6 zV&5qV=>X4dZ#pl ze_Z7|eI0HQ;i(`ul~f~k&-EU^|MtVg1i0z#1V^h`WA?l8?ahO;8)_!DZ{8T0o%4N{ z7q)=YeXAFIQ$JBi-~6$xoAZ_h-cO5ej)mCh*=9+z;9^y@-Me1>Th<7fD1tUOXXkg< z9+v!od;N;;?jkV|XJYCeq1#RruSdv>W!^kGcNfmxjRvC#*GjTBoj5}+ZlAM!yT(&C z6%ZE+eG3RQSDs!V)}6;KnuZA?CWDVBZK+rD73AL-X`HOC_Jhpj0I=U?o&FWu{@AekkD7)_wMiQ+wqG?3;LX9xmeu}E$NVAtLv{1u z_UC+caf0x`3$&^R(y@@EiDk&3bIg2^%)x+->&$j=Ab`Li4)mC4W;zraor1wHL{}2+ zQ5y}yUo_(ScPH5Jf-|&rXldw(7L*= zg0J6|LIqU>d?nosUk-NOwIrld=lRx5p^D<-9A&1TLuuq zzQS|}(*F>g+xm_yRr8bCL1oXzc<=FCC;g*KJ*ShJU-B^$MK8#~(+0oSOufvew6AtK zw!d}Sk55KiOqyv@ZA#9i3J?hlBOb!fw(>)?5AMK{XS4%q;7MB$?KXL4l4hs!V3w3E zX`*MG$}MuW#M){s!I#JyEHW&E5nE)8Y5DXqeH;eLpyYkrT!ZZg^~@l~6(xxS;WT+k z)7q%cjQ5^FFDZ;01_RG}2v44Kkp;x9M~V<&izQl(*!n9F@3t&u-}(k6d=uM z0zd=$5*bhR0$XNEguzAaG%!v8(L&qhfG1FVcminYu%mWxtzY7pEin*N=~v;mgSHv7 z$I?jeFbGtHZ1)JTYa@otL7Mh>+QVHTjGzliitKU}_KG^adr585$+7hMtwcXUQ4t&r z5QI=ub@``MG;NwKvg-t1OFp*sriEg5ghOQeEYfXFMQIp))DhjQc-|h2pFN+yP45RX z!KmLfqI%}T<#J-7h^E9?%;i{jT@0T|5xdk_Az5S0(Gp^dH27`#)7mq~5UV$%#ItL( z7*3^V>Ih}|U|U=6a%GxS5236ikyM#^?FQ&F(1bM_pSy-4Fr8Fh>qWuBiVTDG(vkJ< zADO*gKOHZQ9tRWMetUe`8T7Y;gn6KyqAc~AXHxF>Hg6y3|2z*`eu0V(Ob{|NW@izV zq?Z=t%GNYeP$eP?jc=JaezY{v4?S1080tp_JXOzA$#${ax+n_r!>ZmL6d6HBG=-R9Mu3iC=TfSd0fE~?WpHWP2?PILhy&sHu{J9J}XZ=E9mgvsxIeNb19rYn^xg>_NqmFFW_XX6r zzWP<%yu!qVcMPvqO;oiM@4;z=Z{*^W7$z+xE=|kLRI7Z^Iy~U_*-^N7IXk=KJFB*K zgZ;*Ve*MF}yZJk_2yNja-MG24@nF4IWczpz`VO~yqlB1vSrJ_x*5CZlbo{j#I= zuH~=K^*r4I1aNq9x}WwBnI|vblQ*6{g6Z{XB}XO<}5AA9651@=Kw9* zptET{MF}Re5sS0 zP+FsA@iu}`-FBcSZVKxvtj{^ac}yF; zJ7ki{MT3|)%s9rV1n$zIXS_*&7Z@_zD;qfqGTNYdA4J z5#3YNKwko}4Px-6BEf`){viDv5(!d?P)zZ`nIP`-A4DoSa9*2dGv18{yytRp#u7<% z6mp+}XkVuik354fA3WYZ3si$k>crl{`UQZEcH&+l7Kw?91(o?yArbV-qrq=A|x zhUAZnlBPGYGnDXir%n!iKiZ*y-5{d;$4&+?9@?v{6gLogXDU zPKVA2Vt3?7h+@4SqJ-J?Ri5z%oAlq({m8q?HPpS0y_F0{4y-CTR~ALLHSYE$XlEgjr!i zlD#N=f_*5JzQo`iRxXxJm2vi}8f{G&j&XuFt zZB?Nk$r{!8!6<=-9O@*?K_MlPD@xd48EIi5FBKUZ$pY%Uo~7!r&MbhUpr>)M%jxQB z3ac9$O{L*t2CzY`l9!`AC+ohRb0j;fc(+XGv=i(PAnuLOsyECl73`oGY9E>D$tDgc z!89ahcHmHlD-jzse)Z=jnS{fYy5o{84Dm3-AF!P4@)~{m{N>JbdHg~sK^&rwjn@+! z9omuaDz>gJ!#XMrV#%!7!UFEl!G&`-H_$_^;li<`rtBK|pR-BR#;%%M)d<2DmP~@C z!7M`<4_S=3SETh{MA(pgqPaQ(3`0UI{O2i@EoK*eo-r5>QLdO%a*zmGHk+s$k~I0(NLr_g(py$v z`Fr@#8DhVLXRS+U&UfV;-G`Wgvz1v@GYKEXY@a##l4m&KCC0+uXy#osyr-D)Is!Er z7dV0~ot{@c{Kdl}oK9vX{U`)4g8f^P=(iBX`RCC5A6WH4%Kj^8^~do0KbizXqR7M- zmbXY3`VRzB?vhx}|EnuSnz5(xD<}Lvei;4nf_&7v=+x1V03+BZwqjnB1|jsVGgbcv-}GsiXp-{SYnckP5s)b0=h{3A%1?jNs@ zuBx-6;kr|2&z&D~Jg&n$)QHE&at!l($xhak1#do8P&*+XDnJb?kW^^9Zsc3OXKxB4 zW#6r`KX;oX-tC35yAl!D6y z;MZH6aw-WMK(G8tL4wBm^Qg|1eZ_3>Vq7R^<30b*sXV-QO^Z0L7W*#zuScaf8Gd;; zi?tkum5TBbEc@9A zaYNYEd=VmKXv{y#k68;Ek*xy+O}n)*G%dsB+9=#`K#Le)HyzW)|YG5rN$7mWI0VT`mEqe?aX!? z(pYmi!#biirzn_0pSu=rE52IfiuVTei`g+J@Erc}QHSFFVHW8Y;rhf7BJ+ zj^&kwg=K8)mBlITOXYlP%O1tR_n#`a1za7!HZPumdLo-AjGvQDAq%Vlt$MvXA~jq` zYo+0x&^6xDc(b*znuo+?OT3YhvdYd#c%~zkwW-&byXK)D^ya@C#~=1qtS5V$ZuRflX55g-c{{VkYf_6($12G{Gx zGu>V@OQP9roY#wX%0V|=JQ^wLyCn;rjv|_3Drdf$T9)(HWJy6i>)T7?HyqieR6LBc zTAQWXJJl`s&t}?>qxC%L|5jsz_d3I>{Rse|@?-q|4Ks9 z$$iMA+88@Hnwi@P89V8lTPd0u8$11i+rK{a`$qEcw2|59$N2TxsnTn+!3y7bMg>j; z2BBt#lW*+@5=K~)iC+>6tj3kh3}(SGSC=c6Sa9AJ=ns5Ry9{_oc8i*mjQc}a<_!+V zVOc|=+&ElaHzfZo#ioh<>X1PZX4dfYjzHQt=`8k0*c=B+VzhIgCam z)Pt)~^7m3ff#wv(6EoHvKZ!+h zDenMX)pXqQW3YvSd&4D*wBq+3#&Q6Ki}_RO9-X#S=gHTV>-5fG!1ix)NlkXusV#$ z$g_lF=Tb;0jP2~O&1>G|VoOJJx-Xl9)NZH_n42Opb+jR%FR{j2MkZA|C5t|8wXmk= zQmi{rF5d+>qu2UG$65g0D3UlT$&5P&!Szfa!&AV5cEq89964NK@+BAe=6K;6uLk9E zOIQ3f+0H`+Mmd}|Dufp*#CEKcCx2&gL&VcV|59b>Q7nN6>P>WMk|NPMGV-;7dk=Ag zzlIBY!9-2>n6u8mD@2-6Ly<{C*#!B5_tf?Vk!T^ z+~_~ZVz3)WE|f(-d51wiXxuG@E$>}~cKw`X1{=$lx$r7veJBgOvVL`?zJgEJG}cr0 z!KgWP=$%^3zN*@K<0jZH_#XA>BSAn^?NNQ*a8KtFH0jj;vYX7d+^uu}C0PzFe6(BH zZLV&iWlsUxUcg1fhzu&_#kre6^jkUa*2AdkvGpTeT=;?QFVIh~61TPapz~Qi)Ukgb z4YWVd;7{t{{wpkegd_h)fZ@+>1@~XKLT!>S`3^cFBCx)%Gxw*xR5OW`oIOBjXobHf98Jat?OmEGW->SQ`rae!^Eo)}#lsz)&GELgg z3G$Mv58WM>GMA%Jwpks$7{$GKoEdGkssam=e-_2tCQE&P`+T8%6TE?5+c~%gR=vUb zq0?2)(0{9VNJxVOQL+tUq6y_F>xZ`1ztv{T1?T2de4xA6-yuBuZ!BlQe*xkD z#((%{Abs}3hF*y_{mZCSll{eebiXLtt->-UQED&U>D^9albQPuiNmj7yK5bp`bp5;? zlQ1_1$>l8K#|dVm;3%u*bNKcWu)Y`Ku5e|mlpZN^w(&e9?Y2n8B2z<5YXLWNFp9Sy z3`Z&}0}nMbn79ZXRwd*?u#ec-WFwc_b>V~m$$S2R6Kltb3AI56_yV{vP88^z zXn0rvxm@kj`730bE+v7RfXqt{{*O>09zA8HbL;iT3`gfx*R{u?`YwmHJ34Rj5BqZqn9d8df~L+256EU`(9G~%U$&n<^@>cHS^I7U+l*_B-S)OG~hq^RP*JT^l-k6n7JKtFTbqYoY9|!p5#_10Qlx zL7Dxqrt&*op|zO`iz(9y;TQR|AZ!_vdDEN3x}#+Y8BM+41)fNa<)SYbWT3TBa-YLD z@A>zBP}}2EwdQOGBua1OzxQnI3&r&eZZxh>xU$re9P=pfXKPK!xX3ZG$ebsW3@CHa zS6V=YvY1TW){AOG&&r=+*#vj+N(J8uoR6>p6@|>RgTXOoR|qTA^$b<54|E9jn8YBC zgYP)`eFd)`@TG3hf<)e>8C28}$#O`dQ9p3f+*QHH7ahAnbobxv!J{V6t4iHc7eRw1 zM?<#P{@x)i@H|K~cY>)bMVuP95HhX)B1u*$*(QhHG=>tUbfT7>!uMk&29h1VMUFaZ zm`*2emX|IUmAW&%W5v!E?O0!TLK52xsF8wJ9!h+%X#zqTfQ&aw3b#`rA8j~Z^k?3v ze{%lzJLcOpcc8R2HxQ)&9^9S&&G47x zG4N9|1!)5rce#V|F54xmMJ-QiEct4`CG~|RkXO&IT*nhOrk{XN=H#$u@m(PWjqe7e zRn39O&54-G?2D>!82OdOEXOR@)oj3UpqMLD0Ik$=7#&9I<{V7IL2x>ggLwsWD{(L3 zH!b=B$e9}<-6!o=C6ghwx>KVJrWn zDv}9=7hOoy)~nWD4=|ytM1@AkuF?Qauf!CPQl$+zMqE1?bL|O%7`Lr4@GA))cRnWU zy1(*rdI{MPnV70u!4QBSTc|o+T0b2F5;4Z#T7esIEqGU54YA4Nh#j z?YoEWqpeR_nd1o}fqPtx0C$_!%HnwzS|2g~%-6yEM!kc`8l(auDE4Y`kk=GCC4Bq+ zu#bK^{bjX)i@{?~R~Fy6TU_{EMIaY;>}v4M%)Rqx zE1K@hxh4xGE>j?(CrqQ_lnFu_SlZ~zR&imhw7;6%}W0kt4#1TuGRt^YtK z7~L#MAjM5Y1T6a0sD_pX?T$};GT39`%0$}WKw7G;sF`E~Jg;d_NS|BXD+UvUFkoFk zV~8=R-0i1AmvG~X1m1V)JaocSGO;2{f6pFB!r_B3FXx(m8=8F-q(fG!I50fA8Pa#i zMnabyW_4&kBEH#puRm7r(=OPHbWF-*@pu?_;+!Y-`>44|2=!^zj+P+nE{0h zG{^UO9}YCn$52cRJ^*B{cLdPLlV7n98U;;HFGgZ&dQp;^d_;V4T%4Mgf<{JKT%1~5 z85TS!jR26?hLJvnXyLFZi5PH^Si|_G?U9(Y@Y?{|H?$;@B4cHv0jbRh&2WaPu-!fzzsNQxOZj%B9-O4Dcut3mU4ja48WWi|AY`fio;cWt?bt=k!eML8YG)OOjxs zx68Vm(Q4TX5e5nx#O_TV2I_MMs=%buL8c?EsFv$Gr9YAEDqnRBgwViVegj)kO;uC! zU{*;L&(ylk5tgUfQa$P??Gywgqf%+>LWKI9XY06of6)$RF@N^8_>e9p`@+<7;xpOJ zY5IFN(%#?-*_gRb81H$P;#sCj;N0E8*KA!f({(BGXfo726_pv zx{%qS&9&sn-=>~MM!frHeAsJ`|7o-RVmN=BdP?(qHru}mef}HmqG0S`XY246|C{dj zWGMb4HRP`*|8J-IrLpwlkva* z{YP)}YZ-rk3IAL@xKRS?QAoU`Pnq!0 zuI{d6X_>3(sU*TAfM$V^MzO9*ba~Rl34FZ`BzQ&vNlsC~*YEJA_qXJ$P4?Vw zUNGL(c#3oZut@4Ecw5`~?X5eVZIUE$85NnRXq6#R*(fS-&VuH(q>H$~)CVWFcNWt! z>sOrj4ztP<`}Nk)FX$F&obLVDm}O^07BOkLoKYDgl&MNMyBJUX@V;;{U+fL;J1IH% zY1dWK*Q#VP36>U`?nwBAp-XriQ23zKDEx!!> z-1YtG+1}oISZ#~Pz5SinRgY_1w}1ut&tc}PG>hcLH5-@`-t8<22&%)m_r{$r znHEemq;-K9_lH)Xj46@@Z4y;Uq!iGSM*=!($tO<_qI;KzmiR;E%ZzG-m!?I6QuM++ ze1bh3%S)3YuvUB<5kp0<=P@mOs3s!`AW$;_U+n4xYWkhQTJlNM#w?P`Icubz0pFsd zEZW-I=ddtUL!%H49Lt+DX>i_+|V165T$#M!Ys>+ z3;{U7R4^wh^0$G_b(J#D@a3RDT>$`93V_g0%XVj4+NC$6tQo0&<3ewDS7cz!qes%i zy^TYh9!YWIh}eBOI9DO6Jona{76BHfb_?4}ma5pS)XvrtjMdniA)#j!E)w4dL*}uu zD2e^OMrZT`WcyvB5Q&U;VRfpXtD8H}cpu@|Iy$RIyW>7;GPm*<{eQl}o@>gkqvDrq zPV`X9n9d5nFq2|Xe{-7!pa^+oZ1D_YIW23mm!fuM({(?q4t=-)y4p{EKY9pluKkcz zB7M*P{9{>XxA~F`5AVD(&sr4d<+b$Z;^zi{BMIE2@T4Y2ogS9%ZCQ4`VU(r34$E>X zN0!AL381dbDkVKCpCh2K;M4-cxTuGNaXc?~BCan|)Q{O-&-rzFC2Xp3Us4=%U9I-= z@YJD(8|~~myu!06PGN9plII$%-bn58FcI;KE0=nXKW(g-3om}%{-nW-!3g98PYoU~ z8kxtGSQ~_32lqAq%L|zv#)v2$#kKMgzb2te{uPGJW&wfko?GOaBC8Sgx>X4keBlE1 zdcdx-^i_`55T*LpecV;+BHkOOBnDnUgzzL7*cTLS$8<5lPY~R%dO{JRLgbA@2^lGR zwSF?k_;DCW^b8o;^Oy;54iZ(1Re3VR3YfkerR}M~;i&9Ol@o@Vw#>qMqZ7iQrms7T zWj~KU3jx#VBN4pk%f2;U>8FQ2L}tSr*(0Q3AG|6R5?UAoZy4f`rj2qBim~kDVQen6 zqQXYaPoWda{v?iJ5wzvHSt7Q;1#EQ*mgsRZ?d|>IuUxXE1^#_1^)&C3eAA3S4PoOO ztur4gvzo~FX?2Zy%%ZPIuX?JDSjZ+&fPv585OOM`-N5$##>x0g{$H)j28k1lJ)D2R}CG|wJNHD9kG3zTBiuV!T>3-8uKfqz+px~=aQ_`F<~P=o+5ltosD_l z(G2n*Jhlh!bwmIrbMEYI?0+66!wG5E8%t>Vgt+$%d=0rTVDTAU9oAt20lg<&tgnZh z9?s`{Zll3Qv52ya05b}yct*pgEIpF4lvo51*AQJFcp6CnXebeL8YTka6*bpq?MqQj zA)uEu$ig9clWV0+W$O&6f^(!vP;%NaVNUP)k5aAn3;~*z&}K#f$~uHle0AjTR%&$b zJiP~*hezpiYUDeJM&9GJ?oFujBlL~A_32kK%K}Euq@&EG1y;)bS)fqG)J*Q)gkm7g(gC)ww9r5tmqO7S1Bw_%hG3SAd-s`#!+&`A1SPf7Sd}aTrg!@ z0A2mX%|GOHXJHHhJ)swA->M`?X~uWJKBfP})mCp%(rhI!Z@(LKy;Us1i?4;yP_y+iynFu zU&%=DUsb^O{o)i}U|v4~sFzfw>S5Wj1ePf3=|l8NyOXHQq}!>+&$BsX zLo(v4K`q(GJ*cJd!tx_@AXsT@^a(xuE+4X@pfU1&`tBTAp5@fRlIg?o-6}(a2L21O z_Nq_8xe5ZdOTwICb)#shIo90?G?DZVN;z~2G~o`4f$pSd(eG5}XaFJgn-$ z$E6}lJO75tdhC=eJ;_pg2F%E#620k7YqzmWH>eQzFZ(d$yv*)rD{zd^0IQV8dSt?%6J$Sb>JaA;i- z*?UJ7cG(T{cN-q+tYVCVd9TAifmg9gPUw%0iW6fHbdVs zWb4@P-6E`%UWO4+(V*J6h}!d@KV^K5J7!c=`6adlHt@6Yf@7g4`9iJ!wEXCg7^9^< z^P6`^xT4bbrlU;{zBWzE_k7cW7hTN~e_0i{dld*Z?yFv)D>^Q+_rXEv3oDYq4CtaoA4&wUy;abY2$#h|*#keNtS;48>=ml) zJKhS)ubGheq^&VoPg0+i1QsJ4Kf$DCd@6O`x7Pr7*sUOag4m zMx_Z%W6-XWKxj~w!@H&~L5+&vkPLN?0%k47d>x%5n28zJ=}Ppj{dTo6jQRD-JIg)W zFV@>>c>O7@`7(g*syFAvwof?M&XO(#Gm1}2JE> zm;6M|j5jsKwNWz zK+l*nJs$quDgHZK?k7%_Rhwu6!E%^6Lyk|-gkixbl!A&t<8hMmEQA)ADQ@(OTiZIm zjj-d13SJ!LShpk6F`VBl(5K51d>xY^OuCrR>^<%Lp zr2zRx?z{;KRJ&2Y8gdGEaD;LlHd+C_PcwV_ZDiVFTGJ1}7;OID^sP;Z%NYbzOy&PC*!>&A1bt4~eenhS>!@!IS4Xj>fK3!MN`FzUUn zHpkd;a=Q)dR#UJ$Gms(sBF_Kl=h08+=+uy9uH)xz4<90582toX&u9+y5WtwR%O$Fx zU{Y_yBEXm=nm!x3qvQRjCsdn6wf=+!&BRyWa_2Lva-7!v3jLd@uEAqIpCS_Ho21Ab zFVVmh^c~k0>|~saRe+`&7wS|NR^iq5G%Z!4{PEBya_}%=QJ<^fadD`xT_Cpe;Cb9U z2zJTc@1y*_d}K?^9G5hqF7Sd8T?6@LWJK)-ZD}dhxtIH_ylb`72T5*lHl1=d5fsNB z^1polVSq*}2O|FZsoq~PctBx&DwfNPgxzGO3SC8-onC*|2j{TLf(cWbFi&)c2H*jhYw`d z7kWwM{J`}No&4Q!%^52NS9b*gihVLC5#M4c6(VO2>PaaPW&aBdR#iV&F;*4(pMlyN zF9%S2o7<2NJC|tb@BON+=~iD6!I+t+wnx}DonpJRT+g0iJ~@Wm4sR$3%bUJ^?LC{m zxhkB0FN!#lEz_i+8P!9$dfib(4XVH2z~>H7)RNlQhOTp?CaUR*lt03e+pg5UI-~g} zR&~{92&fD9{vKv-7dJ0C1ySrATzYNG)gY+umR~Xj8I!@$tA7>jSF^2=RW8reV$L;d zgSD$1Cze%%lPWcep@eRPxX1yuqZ)UK(H?$WJ)Z~w9OEcR(d#PSQ+DjOOne>>q3|6k zKOodbP>tV|W*p_9o!(JAZ;6BpNIjA?XG0QZPF`}V{Yzf|bk?p^BU(_5eUarHu-b`h z!6fcs9%e81@|VqpOj6d}aQl*?a#bhKM3qfIAjRgg0}B~j5Pdz$GNV4NR#I^#Aq)ng zd8kl>Rd*m=m!X`hz%eAFkvS%<1P{sW>&Tq>)h24G=!|uAZyKi5d%h+Kx3|&-(>i&j zIC6AcF}eYn4oV?9&>l&Bhy!r@G?n3*qBJ)q;OwcCfK9~GI7!F)N09jMR*gF_ashfJ zWyIuP$qiq_lP7?wO#F}pCPCX3wLD?H2+?I30Izjp3TTE>N}DCi2q9yW2R&kQd?Adl z^wM*L^|6yaTS<_zxsMAdLH3#!6GF(Itr3G{!kmHXwF=X890WQObP(iC3x3UQ(exZ2 zu>0ybRc=|w!4_5SSmffC)x(`_*|?dg!wmPsB*L?5pxN+X)gZMN&ukQhU?(|yLLD+G z54s=R>PryAazLgP1(T7^dxBV%M;~>&q|=sYN@P zCA8=P+be_nSGx6c>eqXf2ifwi3)7t$y}FI`^i}9F_%Wtq*$v~Ym ziMxn04pqSq3e^FApoy$Bs-pG7+hK=+ql*0jH%C8*BDUfqFGXMEZSY7M?8fQ>|Q>M-DmQ$4#TwPV?t#;L;bb-UG z!O1qxQ8S4AsBSr;=GX!_2Zp$ryn36y`rw|xfuE^a6vX%zEMQv)-ev?4mDgF}8ZlyurT?ro zaxxN590kwZ`}r4*iQvzd)kPmV=T};Po6+^#@XntXX#Zmhl~(t^NuvJmlI{P9EdJf9 z>wo;FBDN0J#tsU`{}D_3E06s9xvak!W9ENXm0#4fw%r&)c(2l}RleDU-CKfbVIpHh`GSXRK|#YH5}f$1 zfs( zZ%@-pLp+v-*-@MWZEwbitI^9Zx?Vxg+f_toJFT6~jt(tB=fT-)T*FJzq9I@19N1?P zlq>M%dxK(qML@LsKzF-yqe0&7UGl9b(rSs(bS(HIA`&EpYsCId>`bcgQ(!B?O0W{K?DaU~e9!4*anIB6jR1&=;E!(!qNE2A#y*oH9Ft< zzSMO01Y~rDyUOKpe)FvJ^U}uqY~=l+w)1`C{hKd6`tITa_6jPCFeUmAYX$y(q+TCR z29LJWnY!rmKm#q_O!bp%sk{yQzSxd{{4Ws0KM`Rm3MHTp5q*}KJCB56v}*P{PaMiGSV?k-u;$~e_JjlN0#(i8 zc;;?7ijbKcL|Jte8s$X<&D=UNRQRqHKgSIOruQrhBBfN1(T7lS*jM;f0lt3q8#}8% z_JTDN;N+wI*4@U*L^#9FM2uhjsU5t|OItSU5E z-ppd>fDxgpwRvp37#`d zhK}EC2dj(+XyOu6Cr-2Cz9YanS&v0}=D|of_1pzQZmS|kGOZ{;IOuzIOrkK%*&NGc zLOT2ixh@ghGR107X@v8fuMUS>uwFOx28QrlNnR1_p^7Zo4GXN3I4ftJz8g{CwT1zM z%8%p-0fR%GA18+0BYlbd7;wE9108gx@SvX&>+(t6GOglZ;}#RvC>1n?9L~gSFh(m$ zJ0>%t*=vt|09SM?{WS}E-ZOn|IaPpDuuNY{NflV}bwD7>id<_HCdP?;{dX^wLAX`w z8SHuvPAR%FcIwSPr$!toh_7Y0VLm6M&f2qVRs*DCbmZMhO?4qfs?aY~&Ga^dQ|knk z3R*EohRjqaf~d6v|KNwCa@6qU@b^tOQXpHw_;!H{t&QAZf+Rv4krY1rZX>bx-E8K% zX&?@yT2bxi%&QRg(fM2v)dh7&7XT3v9d+Xa^22WZcxHi{(jISD&cId;estXCnSeku zYi?^8-SstAzqr~p!Ra}BfgXmh!L5|UsKxUJNbE=YB0s;7A`j8FosF(&D0bALgBuT} zUD}Of`B$-TKeY#f2OVM0Xl4NW?CS&QHM6LWA7q@CZUMtLV1v^bMCU(2)cG3BLhTnV zA+6tNixp82uQItln|-QfQ(NLQXD1_-uVtoXS~vK-XPo)|l}D1L^;UUMawfD$SFSTG zfaDuCFBwz5HNjM@j$ih32U+^XD(!oYDD6|A68mt3ixHs$Tn4kWc2Gs~Oy4>G~ zIoQuf&!DYeE~+8QuChONNxsUNc#d7dC|ZB^DxYF| zSfMMr*z;izo8{e{x{2sJty2lb%m`rAodyR(>xa#nbUx@jYKApwhBa=6HEos-zBYFY zmEBKDg|2Wjy|%YK*g``J>Vet|5l&!JiHOOud$79T;VU~fIrH85G+@JSst^r$RgAk2 z_Mn(o6gMjgkJ(9YiiN$OKH7-7C*X}0p*-tq3M;4CRq66|nueX*gyPMjZ1mIOtteP= zC|@l6=N29knNVdRfMg~IvA(&*AoE)C@;PKydta{HBUEW{1HWZR?9O3q6BOM=Cnr5{q=eNR57ivNeYw*cyT%^v^hmhO`7?(Qz>?(XjHmhMhzq`MoX zyCjqnkdOv}{|9$>cK6<0uio#@{mp;IBd9YDpYy!lC!hB@`yWMFIMl~NCr8g#R=#G1 zzNnuf3jo@HE$K357q8@AeQ38@$OZ*CWxQKE8_kWE ztL=v)T;CSjmFj9Yo(}3C7x3$Rz4hOo2rNPs!M)Quri1#BW8vSinNx214$Zj=X+03S zpBoh&QX}{A{W@E%D>g0FLcdaIDJ78K7Xx3^U(o9C$-gwFpY6_YduymPxx+vCCOAfzTkG+MgC#H~6 z1`TwEG%oAnUABHlm44oZre4wK-iZ*DJS7FLLoonkf5_n+Fk7u2{krp=&!|Pb{=&j> z?Zb!N_l+ZN1h6GuAZ&?Hb$(BBeRUIh$;Iqd zLm1a2L>*{IwO^gg*X7ogmm67NwLp(2`tEwCz`fkACNt`DSA1*PQ{7sBD{s2d8(a(B zF=4svU<|gMA`gkaeVK)CPa)H=IF`s_5tpnoVuj?9aelHhc!m@E-`dlyQ$rddLwo~l zLXrGAyr9I zC~>BlA*WYkEmiH|#LN6l3Dt8gDCw+(Yuh%>)NT$Pdnb;vKFb1kwxk=a7lfm6#a8~X zIboJPFuNmp+zrun!8b@2wbP&boNJUBjo(*XOnB^v)~bbitcrqyo(7acWcO6Q|9F;D z4ax9ydW9CYF7Ygdbc9?i-BoRC zxO0%`3;Vz)I&Y9DVk|&O+|(H$^i33w^@My&#zgUqmHETV<-^%ajQC)j`lm~N{d}>7E1$^W(P<3C`*TSK&dXzG zpX!CQt~;$A`^5@zJs_$P->aYp_ZLi{#=O2@uj25$@nw*x1j@iX21d_WBefWn1`duc zyC2_Bu4sW8$oVb@4;p2|Tb|gi;y{)PTnpj9-NR|o^}5gA0E4Vlijyn=8CPKUJ-u+9 z8pU_LduY8gzKWv^7&`e>?XFR1jDTpVyQ6kOx4>9slD&1f9fZ&L4Tvzrwce4&B0wa0 z$|)zu7wjV#eJH|J8Roj)ih%3q5lW5=@He7^3U8p!VD~M$pinS0&OQiivx{Gqe^!PK zP25*8bd=Uyz?bF`=P`K|{90^Jz}!&d^XDQUN_9p)8W9Ket9)5`79{GgHbj6dW^RWr zm%x6e=meKeMv(&_zQ^2`7C$@G6+KJlMF?%)3Q&s@CvfNKRvqtDuX2gPptgwZrzHBmW4 z6il+S@QKKevyV^_!Hiti+KtAD>9=!?Nf|cD9P^gw4p>|Z+7!$4Y!g8!^9H1BKqp=z zmpx6T?^xZoh2b}8f<(l&*E83jucUt{p2))Db_$Kd)|y^EK)bP9U~09&f~Z$9Q?YZL z<`wto^u1+$_`uZ>nWB-YFu_vSA=Z)G)M01miQpDtlHioMr_0ZL`<@xlfkkqbIdP`V zOng7z?1JB^GiB2xgU4p1X!QE<1^H2ndDV1n?Hl+nMF#T2qb8Ly6aAaUIJf4}J)o63 zS!JC%WN+i8o90c}7Sox@+TeyFXln!~BddGUCOHfT?WlAL7iVNkWYG5x zpxEphH#=PBF-KB(m0Pbl7fpVBj znQW`c;%!C%jGF8wGeV6`6_?r+6V6;I`;>RZ8wh#^^ibBat$}{)A+IK^Sadx0KIl=$ z;=En7?6;yVeV@m-U5ww9OVgl6E8u2nq?bZ8YSE+!vM9T7G~Py+xluqeD}5Dk-fM1q z!-z6s-=3Oo4wlBR5aioJW@@F`)v3(gfJVvWIXWSC5Wi^*-_NG<4TJ-j^{p^XUMTj= z`=FN$g@_EC#G~bVmt@5CRxgO7l^0;@ZNeprmYH$DMpd0{)(bKe6Lxnl2s+9l)@xf5 z&v={$r%-P~=U{paM0E2e8uRwVkeE+ChceE7VX~xFgGO61#U3zz&-C>+5h8e#@eKV_ z?IAVi7fer7xB?R-De`ZKP+$-8Y5J-O2jKb`pnBCZI4R3>TD+M$)nvvQk?;phHriIp zs*`i3w8A%`Z7UFo;SdBZQmj?$T0^-p34IJmf~GmzVP8OV8E^S4L0(VHvZ{~VO-wMh zTZZWxA(G+QDt*|7zYW)i#gqy}vQ}!HS6PPSwWLvZnfoF&(?rA(r0`-*Pc#YQ5z}<4 zZm@O!Go&YR)3NsSYp!ev8Y2C?euXipCz8@jr9JCO;M)#iwF`97dOn1Sq{}9v9(Q~N zujKlecP?}b!zSo`X*iB?a7f6XYYKjTA+c+AAYO16x+Wtpg)>*N%V47;fHHrf4>9J;pMP&l0|a z*w1*#OnVWl7gv8!#Do7bsYJ6O&J4H+O~;ue)n%IG?H5Xy^3CC1=LE^!*U<(v!dxB- z`ijo7%DhZ_d@tgvtI>veB^L5|JQl0{q3zi~9YU!yN;S7BI?tS()+;>QJzo2_5Te)RcfXJ=;R4dnzww#iY03(R(MIN5pm5+2{$)6F$JcI9FC z9I?IKW3tnSzlYAh>B?{I%=T$}eR;{x)y>m9P)8oPQd*|sK3TxjvwUCPXSl2x>r+B< zp?=<7CQ^2-{kLlg2ENIjoyXY^`B<>jD6d&9CO7-xLB;0=eEY<=Ej{OtpEkAicfvtz zIuIB|4?Mn5hG8;f^S}`y=0HYu=`ZUiL~fk`>sQ#P7DJyR`hYv4GPq11ge5oN;Dxd@ z7>YtB`5=hhbzy8iiT5L{P>|LMrXgEg^X$dG8S5qift@W1&Dl zs>~p-y8_#A_*Qq%D+^T5v1c|@A98Mg&H=Qzx9>H?hja@T0!3Q&#WWAfO%9!s?GqeH zoR-mXH}8ZUh4(cnW%)_pkfD!o-fSK});w*OwLh$Rwe0!Uigv8Vu4TWt+3|6i)CzMO z8Sie}v3YC6<$JnghUEhS|1#9wPs8oWHx>7r3tlW&-r&Qh>gG;2`+G_;t!usO&Mzez z-)_~T4!HC^k*b`_MwkQe!cpUbKzXn0c?mxIcqQVlb?))$s(oVvF+oY;?Ex|ccHk{6-LAUOuqiJsQ5EuBoCprhj4s}d+Ra+lj zb<%qXcD8ZlW|vTZ->$WO<#gSv^Rem#*Sqe^m&YR;Ubk-GZm$|(L?8sD%`S69QFl0? z&g0yL4|YwkqvRcinl|dZN*u$>d^De&*5>!3m^~Q}Ke2gx$GpEe!&(47<;+0YJTHjc z3YhSZmGD_br>Sh|OlGWJ4y*=NqUoc!qNU1D-+J%8FbDHxXO7~HxhL^H8NauQW+LsG zR<&(ys0$DU>V%FKmT#u8GZ@-C&DSHnK`tj-4?&QKV++k|W`}WB?s^-61rwd; z=H5b7;Drh#&3ESr>R;q|HH3@K8GA`h$0+8&t=26+?D3m2{QfZe$I=tC}3z{bXy8)&}e+W~EXq|BacC`-arp~W6?f3y1-%TIn97&cJhnT0F6 zyL7u;q=R1NY~c*PfGbk5O+$X4#Vpwt;aAcS%ocjT*N1;Og^A({EtL?uDQT%{FfHN2 zF&_br$0#eQHpOxUtQz8;f|Dan?cc?FA^A1fFQ;s{L&=QG42o`Uc|qCV4lHc!(fjM$ zN${)9jp8h#PZ;IhxfRiSrRoG$2aELY(nI`|<6Aw#*U;8@mHQ}Qtv*m>niBJ^eVjWj zn=B9%YV&X$ut&%vL(WAZ73Sth#1IHqf<$%w>U=cC4&m-L`qyPkdAz)TOLT`I3>$) zx{8O9E7OdQ0;Qr@V2m1<)+fFdbDKl^z(1eSPS?~sfTfaRxbda43+lZ2O`R%}bxurd zWB^ds5|)YmSjI`P_TaL;`+ zexaI)unwB$a|j|sVODQCxAL_1cA{L_s~b`>%2k{2=5lnDMvgFuXfnk(Zw9~F{-jf3 zn)|VOg+hx#*zny(^ne818@+2gCis=bD1P!wzS|(Lz4lFsxR%((&TOxNhsn_U=9A8G zard{byNax;eh28zyIz5(MoKlB;WqGU%G?mCbV6!Uq+6eT^CXAEdI$=Ri{?Ti8O=-O zUTH_lb1h)MI1EDIQfKdBD8N~HxYp?#?(QOzJy!8K_%82c0aQU1R3 zHP~}g{KlFgoG-La$le<5IHBHqCmukMdH7Qf&x_|P+JA)*L~1@`q_|tXKaF*}pYHuq z(mx>j`4hsE)y3(1gr~SrgqKXjG9^^T_`MJILj5fpwXHeZqIKlvy=t^UL(!WaPbYkY z)*YMc4{>7I+;^p9co|NvGoanek^Y`Kv|X;qzK5D%-gB`XL?Y?2$@zWiH53HNd|t>e z&04P6I^VLLUTyW?ZzrG4NY?}qPDDJY@}y`2Np6PKc#yATRELZ&1H-IC?SsAes<##w zFq8W72rjrqGP@V=BP}^a>DK!#w!yyc=mbHHDqMI2VQ+2^g*##j=(*3fpAlSc{6a{n zUz>*_nP2GW1KT$yfV?m?gLlu!9npS-eJ(rnhXoKE0Ve;siT|kV@W=aE{&ODw{GO&O zU@+6KX5#Xo5K*eaz*ek+;+MBj#8&%+*nG`@ zF@C&$Jho1!-%eP=ecshQ>17u>J)J+T5L`|xCd>3n`-zG4P-Pcp0yoUQU#p}EO478Z zbTReSw6x1jbQ(u*pn7R-KpP}?@3HUo33qs_c_q`;XY-w-{UwZ061;*h@Si|v7wP2V zz2|k-6uQmGQU$)ToSDz!Vb_&K;Tdx+ox|ARh$}4--(k0L25$l%?vi8BM;9uY2h_uF z1tQx~tZDY+HVQ6?u8}#-p?oqgrsHtQRGR|RY%Q1utYnPLpk`W~ON5w@5SD=gj+0^B z6=A7;@wK*8gkjI;U>q}Cgy)tNmUb^_vfa8e>rMS98 zLo@!7&4n!*Tn@~Ec8s>L7niz-&chjn!1K=fmf}a2_jcXK z>puQ%^f?fd$GBasvBrU2e#@N?>g>8(e6?FPh6>#}#-2ktUCZT5k_ZBJ!Q4Y0G3*Cq zuHnw2{L<6fppi0Vib|;OyR6G00XJ?IujNmlpw_$TzJjybYoDs1T7uvyhSj8N_I~n z&Ic93uhwFwhbq7_BFQGW5R*jK&JxHE95V(XT1vp|o zP&`Wob*A*Nxs@hhm<Q z=ewGEk1gG3nDmH+>BENB6BcuSB{k&CAE!EgWoQ}Y&<1T1osCr?*?~*zP&W?}>*3N5 z)0-%9)3MpWwMMXb?+)+w|mc;lRR-Nnbo5jY*cPa*WZg z-?glHJg5B%+pUODWdjs}{v+$kfZ`+S*kmuTmPp!4cI6_N1&)v_@p5rtL||L9UY~Vv z?p6o0_C`toaiF0%z9KD@h!RX%oYNd%2D)6P%dXS8$XphS0WOMed3k27!vJvJ`y_L7 zN^b(v1e5q1{4$L=drKvc9C~C~lnh6M8j}5;fP4~!Ky-H@G;Q=+>=qgn*AsknCn=QFWzGvm2PR6b06O#hR;`1{yjT^?R~bVI zv*jk#t+E#*%K18|c<)@DP0rY!NHvvfcqWr?PaYH2;2RS&=@-7Tw|mhsCyh*xl+V)c`L|`W9QNe@RVz z4NMg@5#S|H2DS^T4H@;cleflEa^HEi>$fTEbu!LUNz^)~J6|duci4;*tXGGz+@|Jq zNP_DPRZ7rh;(S1T+f49I!5l)dbYE(NSyR@$t3EntMiSVedE_-Xji#c-H|%m%b>n8$ zG4nfnJE%i>slZ^eNPT&o>xfs|P;piXwGDEjhoHm2ee116Z7t*i8qm4z8qE37Z$8XM zL#m^yJ-Fsit5fY?%C#ndjci)$EcjzIu3FEIA*Q&zpgq$3`ceZ$|MneMZ^af(E6CPj zO?IfNt(o?R842Oqxr(OnRu~fk^?K;*g0D!$3AOfdqMTksRK*E}3@L)DQB$VUBui}> zgUXlRD*J0-J~Jm5$v=TV$0DBG1Tvt2JueM_>xBP(%z^T+yj%WHPYl=<_vfAT|ND64 zmm46zdXMlQvB(!}8#|QITQA>1mQI8wY__gX<-)3jtTDhCUlmukstb@Zj$*!09_rLH zXU=8C&1fHs1eE6mCA33qmp9lWzwC?ubR+O2-*JoJW9qudZS)oo@-9EQO|LG;V|~{H z@Ecowy;6NWl#0aa<&B9HTV!=V?_yJgS}rUZ8(&aTn_Yc z(%DRDRJwS$qOhhHr}mVf@Z%iw`SSO%YoXr z**4vbH0aFgDkAEPDx?#&3tR#t@n*@ky4vM<5Jn2yPSPY8Zjx49^FNR+z1IeJbJXF+ z@rZM;G0`E(X2tMYD0pv?osp)rXfSPI8&kmbs!E!!iHVAKH|Wa?8@KXqXqf8R(3ALN zM-i<8L|6Ba#(FEp{;0c3bS1jLi5B%<4pt;QlH3J)by+EoUdH z*JVVy$mVsCZ$36&qrBR}{IH>nb-#o&_i>!%s4YxOq#sYr7J0?|E8kU_w%mN5#G(R^ z_kKB?r=JJ`JRzg+I~#e)7r3B((JZSOhurAWpi#~$#q3};hHz7S!MtMQh9L2ldJE3% zOW=!z>_CXC`Ti!u$dAXKnrrwMa9*HpOB!+GB+vnTLlUN`qNJF3q2};pbfw6Td z=*+0RdJ3pJ-1xPkKJrc&;h(}Nm?I|5xkAE$HIR@=$m4a%L&LPE9ZkHkgSHk9S(`tw zuW-oE+PLv-4VjeCA>&%$1#EsLJL{{&Eq+ya>`7|PhBh9)7nIdXBk!Rm14dGg zz+YhIHue*qN4Rq|>w)@t+NbE0`$jJ)p4aYb>lLRimG#BD`3)S5Tm}(gO14q$Ja}uL z@jDLi`PMky>)Er{$P}VE-0|W>NH+~ykn7TB_d`Sjmy{?F^syc)FDQ4$i`lx46YG~c z7EVPP6wV(A3Q%jsd?#UX1L5XOgCjmlIT3AXk&vi3+ZdoXYSJ-G1rf4GpT`|k*yl$~ z`!~OGriZSdWUUYyOvY0^vXgpbPeOi!^e^9W5J7?riU!!v(P8O&gykj@??`5Hx@ zBpx=OPL=qwQ!~|CpOpyMfiWFJUtAfs4@(U*i#z8w*iZagy6_aDoK;?A43Fr{pDM`< z@lIx6D&NxRzM7_0?qE{H-Kh zE7HI=39bsL`c^(PoeipXm>W`8Y{fEc&VK7~-U$9+y!CL{x)aEOSB_$GK^l}{uipr3 z37xOxxU=Pnj)dAVbNBj`cI9^-k_uODAM-U@xfgq|TUW}@s1 z2rqFXM=vwl*u5iyYHfgkhktFjH28)~^a!i2LIr1SE0c2NaGC|zgut?$-Q)>})Kfa&sIa#@ zT%2BIu5Y+1oXqd`UO&y4nSV}AQz!`Lx7OIEkcsbkwUJzwNIi+?{Vy<2=uD)6{RL)RGskC@RFa8?BO8-U+c6I@i{INy?D&Ek>7U zl+#k~Bn)R-k|ihr7RE*lLPp2_79KCOF_0H!5pLhfe+;={T@dszu6ylqx=)RD)70~& zJ9DC>fj^#QHcDCzcK$%IR=YI08a)-=s(5h48OEDPi=^KltoKuUCwZnlq?!Ebmwx?w z$^)MViAmTQ4giTY7y2-681NmYoJ=;)&SSq>J0JjOzJ5 z2CcTG;ajE~lY$#cFsHi@#^qxNZ5l+ZTOc>RyqJtsj?Jr~7DYb4t?W66Q&R*Oj3lJu z>Fm9;R^czvW?FZZs+7@YoE|g>i4l(X97CTxs8-o85qNB%GOHf_&Xq@8a%+6YYLdrgs<9qvp@8A_u;jjqps)mt%PvDk;8p$k-a}o`5;?V=J$O`L0&fY6$~s8 z&`a_kr6m8C^5~z5)BpN(q#?fO-}az9Pf7d7ynYb_X9IdU6K6L&M=N>}iaqU``viN|EzexRlh%}`u{&*1pvv}z{SzP)>+cl z)b8i75;-$67Y2xnEH_M_0OFTxR3~7ah>$o{)G9DF>RRfan#S6Uk@~k$=wc_JxebvDS z9u>!_2X{bqh%XBAaifh{j@T6p9K~}Cq(Wp_aq5J?u0;F%#EdYN%H6Q~Bn5o1n2eJe z-ZD~L4dHVI;jUnz@eR~ybT}vhCac@-Dm|&>95||QeLFBsES$~p!_Ia`r+9SsM^{O#m0d+OZsZ^P%qpoYHuer?ll zA0O^NG*`mr2DWD3=SThtdzGxJ^9n~C)#v(cO96e8@q$1i2DJ@%%S$A#R7t3M?j^GlWtD(@Ma8|xgHb2c>9+?+sal~-m3?UI)zCC92O7vbn}Bukd;v#wn_ z)b5SUS|x$-QUUk8z_HS8b-`J>$9@<*<|Nf8r?=P(Tvofl93R%XdP57;wWOQ*;Jdq^ z)pLbozI2kU=I<)id43HN8}FZEG#dk6sbR1~HdT-9Gksa0ETtQ+n(f+l5lfxdh#jjY z+yKE_Drm{s56pvTx_q(LXTs4#icZ^HcAmi+uihGgE5vZDuv(Q1&#<50pP{qp=>O;; zg+RZYx!2I~>OFZN!UGbI_B$FNtE2jqFB&2W-{gi-&(315A<`%iDRU=eNen!9Kiy2J z&u!CeHBowdSA=4zvQ2xxYB|AKWKFj|Eu42SbkPT2+8T*m7LVx$)5nbOpBEQSNHl6^qTd=&8?+y%J<&P)n>RKTN()AOkK$@&PfD?Uv2qvShi4?= z%wPOOwI$fWky?wy&&i*7}%S>|k)TDC-(yMKPyx=nVThqHJPjkvQgbNx^YTT-wG>+lP8xvbcd_c?cUra{8Y z?ckqLMIK0oLVdfQib}XlxS@#-N-?q4>mMD+2&*u2MQw%MY6`OiFQz}jT+rdUDn|C> z_1jy(gLup{@x^#u1U8lrxK|zO^cdBvA{yMaH61lrK2!okO`Ous1k(7a2uxk=}Gk z!I>_+34!rnAN8w3U^7t8VQ4wN0l_~dO*Jos)qp$#Q*3A=ukuhGk>M}+J^P7sgO+!% z0XzNJV15)-JdZQ|>7e3I8^|9!ivRTp=I1x@e{22!b%5~iVkSWsXLEWb3p3lFizQSM zP0XJ~sGV^0it6`kId^Fw`-UWdvR0CeFzM??T2cgQEiZSYeiUh7p^*1*Cey-IO1)hi8nI zVs})0fS(y?&AqtF0mKS}0QqM(<+r+d_Kbhbz;`79W-7sbtXbj=)!54%0rxeA%cmx(9Umz2}YPgNVro)7 z2>d-k)aCaAe@xKx%$fi_;6uLg zfD?lf8~K1b`4BVn#wk{htp$E7Kn8FAHq((bkTCX*hpzf#Lcz>7vdn!YYSy_Da-yRm zT6rs~KLL582q7ywBlEBnVM`GOGd&)irhJlKN_v9kb~;FE`3}hUr);6fB`_a=%kU48 z5%?8k{)D>zI$-{0a&oXBzy1f4lWJqo`o921(B9tK zY@q{vh*dH&A_zcCaS?}feTc$v|L{)G2!x0@1>N`Ox{uF=leChuzGO)WyHjQT302=W zY1~avOV@71n)k9_=yXGYjlc|`FVC;K_zxD*q9KM|$!2oi)!H7xWiJK>FRDa!-+oqiONNnm)|S zRf&-DJ=+~{n`8J!06;Q(f7>uOD1DFtOzp`!`Nx}TQ{M}ioY$+ z{2HAa^T!Zz{K)G3{Zi)NzQ|ZOIsaUY!-cM6{w%K(-phe7qK00dqmoxbLO2+aF+u>N z^wDAoX=9>?aO!_*ApL?Bk9pPQ&z;KWuayh^vTs5KQhP~d($Chfd+r=}5)|q*{*&v- z3vhEbDQxRBT0Mn~BgPClGZ((}6$TY9Y%Bw)C_G#~-R|{ugk1VOl7@^br?(zktdrQ1 zW&ML<7J)cu@8givW*p!?yG#1fT;vxDHH9PQ;MI@|fnY^}AB>h`@A%c}3q?CLr>4;vUZ}%tFBdxpfsah zE$vXjnLi=@Rw_MnmBqoBqXPeYC&(D&RVeh3iT>7TBdm;ig0jc}oB_JBY%xkdTWgJS z0!>R|zILTqFT-%s&aPZW;|bE}VHHzPHYL}Gm$YUHS)|PBfdSH`JJ&U;w2~Am$sh_d zNgJ`5FxtmRgx7u&Yf5n9=h6@n&~!`KW?W3^QJoKT8_)b_u$~Rt0NBR`Y)Jg?A=~pN zy+01w{(koVxPkj?r~lVI0+j=X0LKebaHJu~LJZmu_jK<`e$kQfJ%WW)Xhih|n1j_H z`V`KecmkB|oDHo1rjeg}1h@hCKZ{L_tQ?>c9dZo_^Znak4&JPyBI3kZdIF`&L>3Py z$qv1yn)9LWi6Mp5f1kc0td9d0vAxrA@(`_vQVf(MAyePiIp`bmAx1lFwz|2I#M4OR z@G{wFZVND=#T<-!33v0`t*gfqhhMfoE^7Tp&GFrGiZ|2PolPH*VAm4dl98m3n+s8{ zCKI%D1tgP~S)krpz~u=BplACr*yL$0wS55^^%KW3H3q(xgYbU;2BwwtR-S8$Q}mdK z)<`O9OBs=O_$=3t=1Irqnji1eP6m&ev4_Ffa&bwtl4O&u*djU_LgyFNB!a@Z3s5h&o4es66ME9c#Uy|h?a0_e4ehEi&hvD> z1*Dn&1K;FxW}5R=B4sE;it7Wo3`};{DAvVtztG$#bO+RuPwy~1gnWA1uxRGTZ`!1} zaB<7?qR-YWk!f|#A#_L{kAGcXe$^E_BMJ7!EL#fvYCbym3~7LF*g~*Il|T(qShDG^ zN3kWkvo?ymH>S45c%Uwj7*or4X-Yi;T^P2e@)pMsZf40JN>_)}rN4cAgALkCb{x~^ zy!8L?|8?Bad^dL!Z`I#G7hqvwn%-FKx6L5=_)$_xo2^SowNjC zH*|83>SRqgE<_C#fM4FUS*CvhykJZOJ8Hcg9gcd^<=>2d52Wiuc8`cBY|j3eK0D^! z{m`XRmAn81N|QB=Dm{jI?qGt>k$h7)!R~MznCOm_Qm}`TWL%x6exi zEqkz6ZEs(Cok1%#-Pz}Cjnh)HTjNox)a#jg6VnFdzaQ@qxltxiArdyBk={91qX>jw=9=g;^$OJaMz*I(vK%4=yp`l-3YKTe1A zk@WV}!b05kfh%EH#UdAd!S*O*JNQ0#Ln_Qr@en}b=O2z(KzjeG4D>~nP4X^(_?Hgbs@?ETlzO*{Lr zIhnIjeeS^;XVe-L&H7DRT&jul<+Akcvv%1#e6+&QkKhY>C;TsS4^PEvrXIwcPCd-Am)ySsP`IfZVBYUn6Yx2$YDfZBx^qi z#>jgYt~$Fy30w4FAsBN5;p)>jsmcV8xSRa9m!Z?K7kZO)c$r`0lV0;WU4@nhQ@>@w zZw)8n37A0|K7drKEcbbjmqu=A&3b;x--6uz90ukqvZvevkSzZ^Ukn5P%t`zZ zM@9yQ6kxzAYzJ^qz+1Z{Bv5@IpY@G_nNsp74MHPf85kr;_x27*(NVXH4t7Y;Q zMyA3bl8M3QGi*8Q@)NXzZhwgI5$~4JS^GNvU@@>Wak3h7TUCpXDE=B-wH5%Q5Nq2vW3kBa_r}^M9)(N&iwMNy$eT_J@@u zxWj*`Bw4k7cX6e+G8;Ysv=agH-(yWdfC7FY!})bx{19y5|7B_V;%V5}AGPt4A^bno ziIn{qT7GY5e~ZJ<*x6r=G5?X`yTmfe4FGQ1c;y<>^?0Jeh+szoA-|BQEkzupN%t=v zsIMEwA#C8q$Sd3iYRIzS1X~9(2wA8eI)8j(E^M5^&U=$woG+H2(U+i;r-5lrP0S;5n|^`Eom@g=(uGmk!mNvu-G)My&UB2v+hI!KB!S0iQvGNTR^M^A zSyk6}3?V+>Nt^=^JpbWy_=^_y&z!+u2jbrnZJ1=sd4b<0+Vv%7(f^x7TgBem&fsqW z=dUH&f459fv?HUhWX{H9WnR7bcjNTFjVp8?0x}F_S-;vz!O~FDGEzvfxbN_V(#Dv^ zS5LDrA>W+9+OF2F_y~LnEHzVGL0^T4KrLSjT`5CJQ(t@xH3vxr3rA?Sihr?6f3I)n zH&Fbex}9B-0{b12mm;1ZuK|dZ{LoVUVpja8)bkfI@;xX_NVH1+eK95{BeVRE8TlDo zrRHeyk8QU<)d%4L#DmY0oX{&5B!C!t!ZTPzE*obfPcMs&Tme}ii4y0&K9j!;hOPsF zW8`@a-r;cp+;Jy!NvI9ADI__^`(^B@_URi6W<4foi&7v%k9fc;&`T+r5y~c_e9exJ zsG-vl>Y;6w+^`{8_Ae^Pm;5?>J3CMK^$1cox1@sjTKS2jf8a;U}YRb2;X2H>h-Oz6aD_jrdD!7s6% zP>8c1W;fkU-+Oz)!OP}`c9E@;MxXRu*Eu;09=_W}LI+DS~+$aO;QdC+>_t7upa3Lz7wg&P_ zp`cOm!FFxUgQ!1C(=MV9gV~%)Lbuvfy3jzAvbM1Wqt036NAtsda%_WL`DovOGve~i};Z>7k3&`a7nxL7!P{1jU(0Q2}PI59)Zlxr_WDLHf z0YN8%X%ah(ON*eXY&T^V3|#U8=9Wg#KTQ8fD6@bl@y7GK>-sxd*2)RciFS;m45Z|p z;b}DLw64OFtEns1(m}umU*?O1#Qz;3S&yGj5!C>V<7fFtidc2=9}>itU%6@k>m!Wv!vvA#{qI9DLV9LSZhA%x z3Jh(gci_^sl#VB+iY|7ADUxS2?w1~{pYn6dpUG{G8p1%)u{hTU!vSWuA21qF{Hs$hDqJJ@=C5d{4>ABtaW*?|2Qc zXZzXa_3sK}ivjFi6v&Tger`AYvFH45^M3t6|2m$xAD`20OA2xjD4IG_qO&p!fE$r$4<@+CYKTxiW%+Uby{M9C|_qp&K`doMpgaAjnBGUn< zS6R9w!Jip#fv8JlNGA~G38y!B+3Qw+qVF1_x-w3gX!n@@6#LwE^XeBG`8+t-)~Qvb zc1?rQ(q);so3CAB3264xRz_!O6xyl4^U+G1O*hCYf}O!HZhTr!3+Xi zcz{2qn@gqTeFTAnQyjf*^^5=q=PdK&j2I*>9xS7zj36z02iz;)sOR637|@HGPsiR! zf>cU2O|`l>NX|&fO5VlLT|>i2TS3&`K||SDS6W^`MBdy|PBPDdQo4L+uN% z<=$(rYh8P{=@qn z?owP$Qaf6`i)&b&G)O#xOgl+Mpb@n*NXHJF(5I3t6el z1a(s^HSJ#j0S!P6`V|+P3>F^T`cYG{?wlL%aM)# z1M&a=W~$$sUjE*9zw}iVOBWkcVN)BMzki7M9}XhFJ||S9?ba9&HqKB)rh{Qf?n)^$ zu#Smbk>@1>!0I<-XFzGAO^8s+6OxcgA*P*CHr&@_?URLFDAp2;Dks1p6zFUm%^nXY z_8eAiYd6UDVWLda&1Pd(RC>wb(8&oJ8C!wC2tZiTc^X8I0UHZvOhSEec3SFSSQwxQ z(+~IWA7a;Xx9ob}iKG(rN*@|FR;=AO70^$wNmN%8zI_i{b7D`RslI~0ABK3xgNZFZ z0u8`)rtjqQ9EM5aGbdWY>kGb#h244sO$ zVf@N{h{l3^7bxYYh1#%QuN@hi@sxIk>Zl ze5zE*zWO7LMpIx}r#x_FyHef4qBRq`rhV&mB25LBnh>k!6_%^HVK4 z>zjkwEcfPYsM+sW5Q z-S5+rYLc&dt#eL=aGcBQrObF!ScxhZ?@2VBwfIRz_cb{xW0l~hnyv$dwT<~bP*LR# zYNQLO?zujLup59Hp|q4XM)VEr0*hAqubUpezXhHJ!WR3-s4D;$$p4(F|2ptY{#K^` zCnE4y$<}|1%YQNke-~5%|6ep+{PimQ>WThe!Ox%H_!lhwQPkP6n`1!vGNt?&5UG-} z-h2vHF))ac$)-pmqHTwOUiwYaY@$B!qG)WI4uQT8&)?nn$}hV}(t^23HY1203IFBR z{mbo>Imcayun=DXTO1|AA#a3DAIz{k%#up2ris+Rw;3Zo{Z%_mTxyU&LQT~D%loA( z{H-fHUnI&A6Aum?d3b!A}nFuA+ydN7&G z{N|X`30Rqe`omZH#g#Zdw!&VvvPX6cJFl?;Di?Hr=SlwI9NOQlqF zObKLHTaoi_!RJx;C;ceoTq5J!O(E(_NCvx)J&i}BV2~M@+7}dk%gqE>0{te4NJiA| zhdSKuzunM*H+D<5%qZ$^#>Ii3)NRffa%+>krXm0B=n(J1HD+CJ>4o0b5SBiexpSI+%=S>c9`j8!d@-mkqg_@Fjc=6+S-GiCYp>VKS>4619} zV~e8rZ0R#r31|s3xd~DADw!%JLR3Ol?*{PzR19@C-cN6&o4PHAM-rh@3QgbV!~3hL zQeFF%Q4u1Bi|n#5O!{t*uhu^>xeBXH@h+lom})h^lF>{6gbPvv1VTMUCn6rBat;)&1h+eS(OUd6C+hV-zB+>Y&{27)bmJ2>ZohN4870QPz#_1aXVF_X=H$MVOPnnW$gNDsVjCau;q0LTH zrjwF18qF;Lr0%HA3(W#mK7;`wvnzQJb;BGyLnt!}7%l+;_{w)imT=kR)F)q8tsM}4 z)SM`%Z`=X#B&BFLI-^yIG}J9k-(-ALX*mg9%$WT?etNCX%G~^HCPxnFi)})HWJ>jw zrZP>OgT&nu`LvwG(18IZCd=JgBOC&lK(dZ@6$`}jgi7O)&%-e#g-SFyC+-}t4amSt z7b`OfQ{FXIx&t>q2|~Zt$+pDNpm(zvY1KIARJY9`CDjVGmo+eNp|_8B@gn`R3BhMj za%z0tA#pc^nuKr7?3|o+0JIE-Ny7|6w{~OUwUNZnKq`a6ws>tLe36Dp%noV9*uA7R z@D$bCr-4$Fg0><$WyoQ4wHjauk3`Lh8}N(%O>#nLFyCz@`?AD~LP;;k0OQL8kQ+Za z%+gn<#jxZA;ZoT@VMi%vD^Cz016+Zfv%9nJfGk?NfOQIabtZ@)idd(|-L(+WX4O`P zSYZj?x4fduJ17GfzeX!2y8ta$C@B=bjN{IgBz1bw4M-c@(< zeF&{}C}D}5JCwO3;NanVf<68II4f3}8s!SpdXnbcQ!v5|K!q~gh=B?An=QlfhfIA$ zRFoDHkChgcVLcrm_1xjkXm*{k$od#8-E|d{!-I#5S2%M!x1;}_cdHLK1uq{$?}NeT z!}a_FWkjCU^KO55=2jd7_Wkkpbw-+&)yqw#L4B3ue8zsb_Kgo%`zyG(#p8{|WDd>8H%~O0uq*V5RNDboGv(vMCdN94*+^pPE;;_e2=lAB(av9fNu-&_c zP|Y+uI<3@#3);M%4rga+Jyh(~ISKFH&9O25@fMg9wg57)2$cJDGf47ogOHby{ThYO|)7*^~qu52kv!mpS`ho^7|0= z`O5Fh(XVsD=48h^^x&?myEs`3v;Hs4_DsAXXv2UHHY{thmP$nLdJMzRIt>Vnh>UdxN z6uCTUbQ(NncPfXcw4#k{FNE84D?r2T6M(A_s!r9!MHs73WDKuVgH|Vf zqe#_M#r4q;O^fL!$w2$+upBcHVUcz%sa4%do(F{k`i;mzwKSxDKe^ti8Abutk~tDn zOI^CslS~mC&1Y;)t5O+XM9aA(LY^8?O9* zR?5O^GqzW@PhSm33pEF;?T#76wU@mdx}0z;hF_)bT>MBs_`Q6r7JD`+yh!!FE=FTA zE@74zU_M;=u7ffQs|et{J)_<&26*KG=-DkV?7hLK)*j zE6WrP3(WeHd<3brCn+(1l1r&pu%87wU9{)v2G(gwwFmth9Q`m&D%8Z-(K^2IPUm<= znUz@Ij(yhmMoLHSnD4ZolZ<);p09S&#v3{bbNQ$#YQW54M^{)ww+wQ|ZrXxcPK;4bd{Dh2v8-i2H9d3onnV|C zFd>1lIytC;tJXM{@|8c2Td^b$NR^@_YF4LZKZTwpgiVic3mMU`5@00n{Ae z35gl}#!?Sl@V#&Ns)g|$ez*=}J3Ffyw$w4Z!*hIq`f?0$ZrY@uCKWXi?MyN5s=O!R z{BnSkak%$ z6yys%{ByAoWQlEEvnW-{QGRcpi`-2v+zH!^$yxQf)fB>I6RDUqA{{=khy2`e9%X*F36Znj&w#fnbmpopqx=x=*6 zYKc7ztwPw-!(l0e8g;>=xoa_&CWW!#FoMS|M$2K0z)|m0nB(HNg&G9#gx#OE!07b{ zlK|xRQ^B|T5ISMAvHHb6CLH;phFMzzk;DQ4MpeC1RnsKaB4|Mq5=35E(v>R(Rsa=Y zzdkm8-X)I^_$H{(6Rr%*he1fQ0;ISZO!$L}tv~*Cl4W=tMY~u5lCz^rG7)UEABl>O zX!s$MTx4|LBJ>CAcW$Rn+}H06UG8G@t#tvIKy_I}Xf7b04nq59ybgwPa0RVqSk^Nl z>QVwY&umezQF}dLL%qPYh!QyvhRFGkI3xT*F~|atu(4k{O?QVIlp5+xq93<^+Iya_{UvOlf3AGO z{V)J`L;5}L@~_@D{}WmK-_Ki={$<-{2~$H8Q>Q;BtVOF#*da3_e7REAKnxWrkfSII zkno#r@-Gur*(gF1!s$AbtUIkjU&~|@jYl9a;sb=3)Zw@b0I3D>gc$0a7=k>R(p$`V zI&3`3#9TTP?3NoL#290mKw2}749x=?t*d5)Lz9(1C=X) zO#k`H_3~zTe-Ka>WW&aGwS*0S7A-z62+Syz+)}i)f!wkl3@QEdr(I>}9k;;%17v^( zlX1-#wL}|cU}~{eqATD4{18nsNjB|K{G26T;CB17hqY(MBO4W-aL= zAho=-MhfTX)nB{GRtU0b!mAa+DS515w2UK6{M?W#O$b8L!HOt?Mg~WeXo`-YrPM9Y z-RYS}fO{_+!$;2>CQKdkP!lu(4`z zIOej%q;7C@S$APF_BYjRadU%}ht;2M&y4O*ZqN7NnYiEYA70%}1CK@lUi>57?Un5UBV(3UJ#SF*%J2{^=imT8p4V1{%FtE?6SKJ&Y$ZW|1P zHKf&E@`OQkMAt6u)g5*u(1al#Q7utpCKP4rV)Jv1K?vf{i$-cYR_>_SnDrraUU^N* zhT43dPh8HV|B|C9*Tt5dP5~;zuEQ3adY-j1I93hePJo`OQ&|o`gdBF&y8+YhjWMUww{oQMJ@v|fSL1c zN@R;391k0o9#mIOOBC&Kyr>;B_~^A$QX+&~m1l8tu8nS$XA+;4>w{W}A3Dy}_HTW@ zq*q>$nr~vlXNk){o{K5g@*Po z-#niqbr}WfYE*N=q^9Hr5JV}JN)=gX5_ntH=s_aFMw?su zu!~|&N}YTYC3o`jZ^^yix0jEPIapNf!w|>A{h8({+IpxDj{;sy_m|6hj=tu6aqd7l zD+n(5%7G>CE9W_n-R#J?32(?xN;k2hTUP}hegNzH#Cqed{t$FiKOMy3dY&$2uYW$xjwi7&20m2QzAcdnRL%`MT}Wj7lN<+`%un0_H=w( zGg4J`A!Am3SP~MpDaoSrZu6|R%|yh+umrReDh8nN5@ItflPl&ek4(zzf%$HqozRY) z!D5D2AUuVU^Mx3I`nabaP)ux ziB?>T6lFWU_>rQ0Q@Vb+N^Xzfn;EADXWU}R>C2K?066yoI(Xg}EkL>)zgvHrenl|z z+Jm&cl_AxTNYQ=(j=TI6i|r-j;MZv2_a`~%2$&s+Nd#)pNL641v!t}wqA*qL>Iv)c zp{nz<7WEN?`(jQZ+g+-&j3ZfbOkDo>RD)wAIt6R3o^c5B7XS2o=7d|C@p+($;w4F_ z>a2w7E;oawuPXa>D~$#q_6Z^&-1 z0t`2W;+Twb3^2p~JWLRVYNsec44OajgMGQ7c4av=+Nkea<1}c5CE}_WZ?Tzf31N{w zZ0A&J$$tnTN!nFEX-^^1=8uJB#5#?H8nR-_i)W``bkwJ(>CmLa2gQ_@jhi!4uR#t9 z4`dr*BesS$B48pMpV>3p`ekBHc;IBw3CdV$hIV60wtBem=u(eM@Mxg6UXBE4cq<~k z&@Vg|Mzn#`y;`GYRitnr2QgY9n0UFVaZMF*#tQyeKw?xk{EG2|33-;8*0p4(B88H- z<6Q=oLfQx6K8iy@*}q zHEF$K0uS(M4K=Q?aB&)QHM8HEHh!AGMyo{+}fOyusOA71wVXACL$-@i1~&= zkGa*s!-OD7j!t5F8t8>JH)8vW+VCYsz?%CHR*)2$-`)gSfgN(hTS3XY zsHpmcp3#}!!4wW8b+&H@72H?Xq&2?<0w;}5>nIIuQ>Qz2%Da z7!d4tbUEc|C=UjkrYNH1E)CAh8+&=yY~;pFq3^PZ3%8bflE%YzI(Mq3I^MQu5W{xP=$>f%y8bA|12ZZtDz&x3m6d!68)YW9 z9-I7nBuS`k+kUehR5a4tHxDlvH+mAp^$Q*%9`U_a;^|sjStpd-p^3yGH*o^K+L+*D zYU`nW?AZ$M{;I5ydo<9^AE%+MQe@_=X!g9({hfBxqi|`TyVC>%Z;$*S2BET>x&W0E z-lAtTPzr&jme#LnL?t&iM4jDU z&R#BJYg@N`n5vfz-@iN7zE`t^7#XN;CnipHfvljDOq~?sF@+_RKTQu!BE*YQz_y|0F=U&r0rRF z$xgtcl-;G^2t|u#thl6{i99Pk&x9aRmSPv}TB43QjN(f)YYo(uY_Nn`eOg^sd)UG+ zs_nVg)))vXEIQK?OC}qh(+`SAe5CPX)VLkHTxHD-RmBpzO3YQF-B89SqCoOtoyBOD zBH0&p^0Xp|9#gF>W<-6G2M3bxat1SDH#vxQyYIqf54~N;NYj%A_ln18<$%`Z%5;5D zt0Pn)(8NuM91pJ`N_rAFr<$q78DvAb2uC3@e$!6hP`HELPZ(DDa?3#%1qij^#RaR; zxpvu*-V3SAUuI>1UcL2eAH%sgrhfIYkD2|@NdErWj>i88h5j6pbo&3pp!|Id`d2np zkGCl^2}B@WSPRmb8kiWQ?fB|n1?8wS|5l`hb>8Ur-;0R{6Ab?Ox-s$>PoxmVSg zV3_u85pH&St@yI?_shwTe{Ne&mFo6Vt-a}(W06w!!*(g)QWIM&X=$C`uBKw~Az3H0 zlWqoCl#if z^o&J=wHsa}<_|l26x~Zr5j$@JlKTd_H1sJh%$7dCKVsPoYh;s>tx%z>Sc+|BWgB2Z ze8wg!_Fa4<9jR$`Mee5Nxcipw?E#4%QaN3wmPPf=v<9DX?-fbAWD^-aA+^<;D!kf= zH05jLO%CZ07LQa%mr3Q1QJ9V7+9fG&oaD{WA)~gx#7L!k05jG$DADZH1&P z{8Rz>8e`xPU!*iA#k>U=WRaGc-%RL}5+$Kt(Lc5(j-l7pwK%FMAQ}u>p z_hTAKMG{fYdx2dWZ2gw9{Xf>%BcqWq|@v4^txTT{S6xByyy5TH^o^fWvoa6 z>l#Uh)-ZIH_Xi(&00XkKvTa8TrW7@vHwezzssn%A4QC`xNFukCOvTs<+5lJoSxa3W zowb6Ye5AGr6sUM--^tE=+rlB)0UFqiF2c*R>vyY%jIeJ}I3TuKP4}B1FZ3_+_`*s# z**u*rjG<}s?o9J`@>tBR(io;6qoYc&+rQQZ;P?3Nxw*J~){P&V>EvP7xG;)Y!*S;h zIbLbluEw_h(kq*}K2=pT-#}F%i^Tk&DXWp^NN`(kK1g1-v>fSvV{99eS9{pg>Ly$|;ki#0?%?Z>EmK4k0fqw4R*3 zTT3JQT%d9HjG(PXHUBzrksBG)i19T9=g=U_Jg3#murnUuBGQudm!ESRjr=~{Lq*_x z?+YgIaBle}AmE>l_+J-%eOLmYx303Y`Cz|za|Q;GPd)c^!I@9VZQjND3jaLn;Wy|z z$vxV_ee(ryf=oQUN%_2(u%x}d)2}3Ln~ISEDB2OZ zettRyDH8V(Np7U&a79#8MT?>ns2wiIdF@v5yj76QoB5a{^;+Ga=LA1-$ykC!w{ym6 z3hY>#)Q!1bwf(W@iSj3=_ewjX0nsF92l2$FJ*Q^~Df||6*{8J|{C3YgX_F1v``W3+ zNDoxC&`OerY2GV3j3+Ku&P}0Jb(t#-%gl@dW|tS*>M+5Q5IxAZp7_^5CqB+;n5pul z*hePFt2i>#5+fu3AM2&LoeU2x)})u_#T}ytZZaz}k+5n*7d}dV+@=0B zN@`z2cQf>(Fel-6r`xLjYct6oL(1PzAph!W@sF+}fWOWPjDIgH$a*S#nAg$&wpsks z{cpFwwu$tv9?$@QAU^;B06;$06hKu476ANL^9kF>_3QKRQ{=zd8v0*K^#9ItKZXc@ zJo)z?{C~Tazs4Q>^;-UCgTO!XPJda}zt3TREvLkz@o(3L=DYpc5bFDe?_l3DJk$1U zL~4_tU$sM45yS3h!F?Ct#Wx2e`I|nJa$I@01pYL$8xuPH3_kln#}90T04E) zvg471p;t4XL_J`|4l(<((#qyVc86mgeT2JCiH5}*pH}pz_W9G&yUipPA+;*GqY6Wz zTPb6=BG_swuh@@L1YTixXDfIa74z9z6ju6cDlL=+mg=SP=tj)%N+Ch;r}rk;1J^+B z@J7UM;UF5rCpN+XUB%L427IJ(vI(xwd*5&KAD(+Zt-&szM%4t6ju7Frn;vSY^}b;v zXh!KfD+{MrfRLefS+4d z!y`3C-A0Fa2|J6sg@yI==(1)+2r=AXf1H`YoKC?yk#iO^lragq^9X!u6Hvxo4u04X zF39^PI&F_P(VGvI7Vou{wG-DE|CI(DH9?`t&do6FM3RVoEnaX4 z)wXoygZW7xM7hyNlA`aB2ep%)?1u#g=`a($&Hfzvt|Yi;E%(n>-ug9QVc+uyKJ+#D zfceD~KyXM2^SOS4#Y9d*7o^v+@&n{l z{~FO{DXGt5Q?`U9b{dlWP(m9jvt4B{Hi44o5kN-Y@=jU_E#k#RT(N+$bcT<7s5DF` z6kJz%MZ*n;-Z4JUQK?Kop|F~Prw|p3VTU$`5#fcJQ*(GLrWq5P>ie3Vvdk@_*Lvpb zNKpy5P2=VcrjYA@bwGe!@Z_hK`UFJYjGbm|{No)!-9h91y5xNcxu+XASkiIj?V3_a zvK)uLK2dk>ib9_aQ?E0s22utpK}BwF*gDX)B!4AdH}0YitHp&VU?2y3MMEqAEsqec zbL>`>Zt!QS*kyXALyQgH(LOx=grm`n+c_u0AWm&9_62;IVP=qEqbO4!ngW_X5~;22 z7d_c*9-<5!b}0q;3K**dPszs9WOXDPoODx$|bR) z-lsr=Wz5V{a#^rc9?o^$@bQDD+bx;f4&PANcDv|M{7M~1VUx-XbA3b=b#q(UB`A{~ zG=h)4tmPCjo3EC*Q&+QB_3pvtP+Bw7>p+DHz`*{AwNM6A zhUEOgxU5^o4(ZpELYi=l+Edx-kZBLKD0r(? zJyhf^`}&OoY}m3IIHoItK* z=@5oAfWdHMjT`g`h=lp!{8ptibEoe;v#(Lz+}f^XCDIRe>(qH=wUNN0dS4#3X|G&| z-`C3DyP)AEn(haecze2IyY3@Tw!hP^^pD9~cy_Uq2gZoIb#C#cw1)X~0!M!rbMSmT z?|hqqCb`#p`INcYNV|`FKj1?i*R__2?m_FZ$`>?g>3~aOZ-akjc4gJwx?!;6YyYDY z+vf!w_(v!1G#ngG~Am+I}QTnWaHg_ z2}coF<#r%Gg(fpj>&v`J$*VOSoSzhaLQ#zGpMqIb?4>=t<_zxzWUTgPmhQ54b58}H1~r#F(<*Evm902^Mys(CX`;^BHvH+X9Ku5Edc*x(PD7LDA;-v$htg>z804H_jQbm|1XB~)pZ>9~@ynm#Z}v0&$z0=43|7h1=|ASZel%bI$L;Ua zuDEa77Qe2AkH#hBk79|3T8wKG>62APzRX)A*O9L zOQd-Mqj(X*hWTl~q)-8!5#XK?<`;Y~v!&FHAJ1Znna_l)>734oFO!_N*^*q@5~YY8 z;-g8|SFkq%jrbU#m$G`f+FCj~A@cStQxJzD;_Xn+iyi{ysv@^rpIiOPf@_NC&1P3H zWz)%`5}bll(Csk0xV4%bI63Zh^@&>)n~=f-3=NSFWNp>}Ng9u-TE5)`Jd432L-_c8 zkgRU)#$)R5v4@l#jbVcqBA-~J>B9J92ZxbK*s-eTz!KzR4n8>DTabyjG7H=Rey+)d z%%!cqKtVm3KI`C0OKa%4egwYcWKX-3@)CGhA{wfDV3x?tfoD zXHasH85e>ag_Bm8KFG2~uJNQ^l?|be^t3^$hR{!C#-y!19G)oh=%n?r9^Ig_Vy!H= z7W?w5T4hgt?{q~=a^yXs`<`?RvRseBxG<<+;2w6M%|mat$^sG>LuE;PYAKD|vYhrXNR#RC z7Iym%E!+E}cA@m^pkSH;l7_zhUI zK{x1K-QTF%fNEiSNk6s!oZtB|jM92{@XL`YYGayM3&_px!{R~kq#}o9rh{am*I6c7>>a1wKg7UD6-K7R+}!;%LC@fDa<{tKZqrRQ zvc~T-O$FDMsJS3`x~qS2;h()!H9S;j`080Bbgc0T-TcVbcDtBsPIfW1jpnbvZ%1Wl z+YP?LWgDR4=W%Bl3%i_J22Vo3XS;GOX~9{@5zK|eGX4o&^dZ^%YyC}yZ?pcgOEQDm zF$k&w-oP>yhG=LXHg7r<3GCT(L89`kF?nYq-}Q3n50w#%UStLvlou+bT1W{nglACg|qR*y?3pg*|cO_$FFL zwIH|D1zR|QuXowJ!scN>*n`9-EXe>ZTO+WZX8I11WLF1hidcjpb(pvlPqzRHmY9b? znZVz}pTFC20jzN2+jw!WW%0aSFWl7{;i0kXw<}}Kr`)h2OB1!M@}~wcv6AmL2&ss= zU(lP1A6Q6?6Mqhz@&nlEbANbkz{@J4I6AkOM8xIlsS-D_;yJDFf9Bu8NiDzDfImS! zN`2W@iB3PY*y8R{5kD~J=?~<7@*Vp8EM=yWV%*4*<2KAANoKk_?BIYvZr7cEERHdV zeCjKdy_Jz2bybXD_N$|^f#356b6AuFq-8LVV#OoAe6;pTo0vyrW7W|rL%FF!#T01ZwAnk2yqfP@sP z5CDRdTc+m=zBx{IwlTjmmH(;EeDh`E<>8Zk$;SPvt)Hyba%t8DV)rokQ21gwWWXEK zJ4tVKq6@092Fwmd$&vWrH~2YzctI!)UCr(_z4kRd?^unp;W-&9;Gdhub``n=7HV6y zwYK?B@ac!bJD$LF7C?$Z%{4|Ry}}imJ?2YwSAaekgTvODvD|eq^2y_AOj(vwM~>C#%0ivyiGl0D$M;i8Bb zn&mRbk&dWHVA#CWXERr-*im+P+5!I2Qiia*tyF%hNY7c;B5}IqkI^2VnMuSYD6yChebM|1ETYaE;+Zr|)mquE zF|^{R7!T~V&MVliIr!6v2avGHhdrGj?PV>bQuXxCH4Y_JGcP_Q#wFXga6FJ5Zkq0V zc_YgmN=i?yIuLN{mVI;!1V#RFjDQ@vkka*u8lTTfLe@q^O*r62oA<#fLM0*q@eiTE zr)oP(9dY*aO_%LmOutIbp``pI=1OU|#IVXD(=s6`p3*@7${68f$TFFhY#>>|eBa1q zWFm$nW_+%UB$ZX0w5%MyVN7Y+Kng59Tek9^Xhynfr!w`$|D-Z-McsFzG|ewxm_jcH z4ZH`f6rF+lx%=@M-F?*5h4Y0~$pMQ)42oJ{1Uwpg&x&Epx;NU)lYC*zd9;xg2S=W?aw@J7Bzj#M)Zia**U(a<{StM<5FY>Kd|)rzbr(UqidtiKgt z+23Z9c`mTJBfBTEduyDv?!;IZJaOOQA7EPzFq!dTj>-rP~zs?{FOe*Fga>&z84XSH(ZbN_B#)qj3FVGDapC@;M>t!J;Q?kmzbj*ub!!sBm zTX{4L?#=QJQH4>iMT;6CnIK_y(K5Yi;UFonxd1^Ib-vQ4FTZM%ii6Dyx(BkX^=N|Y zOhkx;gNT!aTn-?7nV3yW=N7O?$t<#biIIT3ZR=FlERvlh3EeK;)3(jHstfVzWWxoD zw;;Xqvihotg~vQdn#LAhz_sEnpl8u{mc4347KnFZMU&KDc56cNi5r>>34g`#5GUo0 zkLYM^{bW8&nZ(jR#~?v|1;L~@1XoD?C*(o-wGePEvtgVm{i#U!A>ZneMDuotlL7pQtrkd%r%q=aM457S317Und+T^# zsMjMEWT!x1h@Ku1F@AdXO+h}U4QcP&5M-Na($RAtpm_`~)F82brcnut!_ZXrg!t!$ zJ*_D(!Kda|_U+JqG1l(wx#}ZCA7Ny+fIRi(V;bGBz>&#?ys-@`B!D7EQWyGw;8dCd z?5&J#elb8rX{b_}kcpA~Zy>*rZCWCsM&kq7@P5}9<{uYu{Z8zEoKXEwqyOIzcKm-t zL{T>Vn7{mIf+*)|Yh>#5$7y2qe{N-iwaR7#B_SfD$QM=h71%(7Bn%?!Yz!}7f@@*7 zjPN=FZFAfQY`c*;AjIG{?9`kM1Lh;ad%1mxHs`Z1MRw(i$`1IN*npAeN$fCsp|VeB zbM7yla4UOFmCsOh2aCjE9EjQZbp2zg^@^RphoM@-y|Qy|#cTeA?+_0w+^8zr_y|0- zUsbWdKqvOWZ7S>yREtqXF$o$pp%|&k)@0szs&DtnKeurTu~4ub=PC#9ewoa&?hBbv z@aPJ~xs=N|7Xvsb3}cp;P#smonlp;d#4-pBbMh)8i~^WL8m*r(F&T)aD}iI2o(hyI zN+4riNE+#Zfl&QLy}LA|Ey(UWGuBCuz4!zYn`VWw3M9h$44t52D*dO{iMyIQh zv(JWOj97ZZK|P+FHAgB_20%jn0!E*G1CuMDjwk|ir3^=?atP zI62}z4Ot+!2ABantlEI2P+lwpC^9ld4+LaHXT&H_@!D)aertAuZTHq%4HX)}^tGMNAMG{i>Xk_n?HyLriAQX~Z(5>y zXr4DaQ0<4~2g~Q^E(02mD)q5kkgZ5?Y(q&oew4s=6t(ozfo?UH2*#QX&;HSraC2jK zvtI6=+`guq^i$Dcq9Hp)%VHr7-NE0xA&S{W(%f2u|I+pvF=A)L(F==U%B5@LAkLeH zR@0rijQT+y)9~=3IZpBwIDOrR@QCPk>+JY9U;ZN&U&OGFdQQ6E?MwSr zG4*UtaM~IPe?E`Nst3KZMi^P1{)PX0*2) zeM2Dp1Zl3!^A-rv0MpJ5p?Np@)pH_3I-$uZa%--7<+Vmzf#hdHSI_`>0>ucCNP%6Y z8kcygW&)}{e5={_-g@()+l&nwS;g9c3VOVk>-s6@$!pI0nyPs17#)q}1E4imbSwZ;g z3Z=2R(L0V&yp+E)B1Rx-G7Bpiit3^u1eb}Y74VS)bqAb+&bS8h^#WZOAA*XwC>G9$ zlDaQiHx(H1RU6!|Qrz&0)r@oD2MUltC9VOhOv z1HhMo(FjGIV;L}}xq?8Lz4hyiAH)V?ZE8WiiizP}G6FE7mD4;3voXyvAPmO+I1>7` z9|mxK=-vrmt7OvXYL4{xuk_s_nsQ`kk5GaoZFiw)kdpi@NEo!0G+9{ z0tl#3Xge}TKPtM(q3A7N8tM7p$69JH4t9zeYxm|X&-iay9IiDKCu5X|uxqiEkPgxi zk6iu9z0tu3Fon*F-PRmk-yxe}NfEjHQi`tmK-g2W8uN<;&0R0kg;3M}FtG`Yu)=BlgKtswYn!G|5$K6NtStu=BK7lJc1 z2mwyIA_dJxSEt8vVug6-bU0^7QHQ6ylndn~7u$zdB&!e++gY{&bWsN$KGp<|a~Lb5 z3@HOVx5YU}bAwf#NX|Nu(z&e^j!trcluv~#p@=D!c%2n^`GW}R*2gMe#m-aZ1P5LN zA3Q*ZZ!g)(Lx%EiYTMdl6FRv(om6zl!Q`})%f50@bI~h&$Ijg zdc8hhjo0_{KJRnSJ@?#m&pn^Y>qp{zTDI@sb~D2L(QKWAc?C&NZY}YfIc7z0pu^Ah zjOO1qo42Z-sO@{w`qD|^`?psS&zIdU|FV2l-p0Qt!+LB30)@&wMINjE5!PSXBT^x_ z^!NBbf4U8M>VA0ybZOmmzTv1$=;%odStl0m@VB6Fg}ud#TernZ3eB2M$z(B42 z2amkSSASaV=7`j36_IDVc%16Ctv8bIoH|4H>7#9Ni(-!+oS04g?~c39jcqCw2C@a8 zPF&J){ot0n)kH8;WsJ=ksotOY$)X&RD(M`h3IS&nzhByQvrA+0XS34r?+l3(`GiNd zuar&|ogn*s)Kq;*eck#?p=W(0H)NO|Qhgt4wqv zNYB9+!k^r(6(ky( zlb(7lewa3QLBYo6|GG+rGoNob5}R^oh4lO_N_nAcz8CqhH*TxB}FZ~5f|!5dYhD6ycb=- zuh=JWYE(X-G3Topi;xgyNPn%qn;U$V;6|M+ia=)OThbT)XyBv==)O> zCRArC`VcHq-fvSe51ui*rcv7iwg(yL*EE&*&(8k(P3P4DNH@0Z{E>LmFETu zuS{A@uXXAw?Ri%ICLp!vRQ#dINyTPMjnG22OGbfiWvwwNc zAijM6><0#!XXk!xoYOcb;8?~BlfJ|0mlw*JXUD}vLk(=l0Yq~yk^TAi$ z@4}S1b&52$iXKY04rt*B6`OwI+t;lx+j7PqQj~6)nYibZ3}I6iS5$)hxaN%RkaUY< z8+cETbPLp&yo9TA7H9LJ4SVx-Qp>Uv3j!q8tt?%nz5j5sFK51xzV1siHL=<&=R!6l zv^VFO{8Vfn+qWz!@j&lw!RAA@Qg?PGC_Ohn(_kIOs}@+OadypVA1{f$4|Js(i(Uuk z92@H;9%fqh%2YBmG?4$@3c0;eeUDRl%(gAn(Y}B4#NO)fvnH5oJ-NBFdi2T*#kZet zGvC$HIifnS`bK8l_VbzHHb47xk4;=%aUw6^&+hz_T0*lgYTB(UaVph$aBxY;mkFx= z{-ajw1`}sL>T-Y?&ITT$#2rYwDx++G{13Dy(80W``~c znzGm@d-mfwx^Y)0?{!#k&M0q-jX=h`%F@K>IilM&-Tw+#R8MPsqV=Qh?ov66zo#qa z$>jWaZ%Y&^%t^a0JN^&pBFE8#l>#Z?EP9$atxWq`)}tfRCW49Jfp&QGFVCaX&aE~NZ=(XpZoZu-e~`0+ZXBPnuXjD9dh&K9*<+IR_-@NR#VW^r-*Dy|n%FGSyn z9XOvWGe%uz&g9ftYOefiYen*ZZrNY;=8WbT&gj`ZL7y}xF3KV!JH&z)>{HAuj;KYd&b&pzGAJt@KWo2fG`RN45CC;>0Q{4^Lldco( zyMxuazt|aF5WVoMtHnFxWUJ4J6j|3FPv5n+)F<5%y77x|^u#Rlm&z~CS6=SixKBbj z_D#+7CM(X58x@?Yo4Kr#-~aAPuh{AzUhwgE?PK1Hx3$%)?dQ)IA-vYs7mcvvsugTs zdXL*?<*KBtS=~g#G0P&pR#wdG-7&`c-e}4CJ%Zy$PrDQqI(xGJxG68r>^`&AP^|oB zveXZGZI4G*N$rC3rmc1Eoc`(lj_au&D`Z9x!CZLB4e!G)3GI-bp`*-q{hGjaU5o?jpG>K{(u^>zD?oBsIBuKJ z-Z?S6fGDc7dMUT4a+1VYix)1F|J>@@p~&aj_IhGK=tY%ela@vGE9;N#nZmne+LVll zq>*#iD)DXIb>;Jjj^C3!rN!59Hy@8XoUUMBAyEB{f8X^HOP?>0HtM?~!WmCpUm zWw)4OP2pQg&{Cb(`mpZ+N)Pts`95q6&6$ zweqF4rhaSX;+^vF{W$}r>aiM~^IAVX73rCktk?MA=>3)3JFk0u7|r`+%KeX1Z6|ve z_$U}u_|w;H93gLPw>sL|8*wH=yI^| z#Ki|cww4B1Oz9Jlu$!(d?Y4nia2K&EnRlj%sB`S5Q>AIw!pqd+5A^hDE85u9NJMG& zsE1t{X^?d9o3h54tJ9ywWnTDhZklU!<>Ki_`^-evTz_xdwPn=h+shvQ%@x|c(0{3` z{)e`}>y^3J9;P1K{4nR)e?cOftAdqY%2&HD5i0zgEtC1+(c*W{YO-?(kGdwl_J2P% zPtFUt(AzYE&O6sTZ z*5{TkRSD%28NKzxvqO*XZ#&}JRlLyMq1jxkt81nDm=NcwN`#NoPuKI@UbZqMoa=Uw z!mlqq?L0Q8EXu!Sa_rk~V7>E3q>|nIpSyITl?`$nEfz;#pJjb$70<3--q)vg&DBVn zVP;vC>01_%YIwz`EjK<(Rq^oQm8Hv)cK38xc#P|gGcuaEx^B+7gvforOcFHi9MBZ2 zdL=jBEnQZn^+wyJvwGq6aivjX#5Kxn7TStBq%e^X2qaM8cEvN|v-<-Z>&Q z(oOP3v6ww)>sIx~36a_B91nS$#f-=N1lMe;GMT zgHv^3)oLmyq^sUX86H-sGSKlDQE()uM6cv(zTojYA{Q6$R&w;16jJzl?zn*U>JIx; z_nEJ770^3c)V}EcT!EtGJ-4RZuAH_-{pEiaKS!NjG=Z??&MdyL@Wv_aOWy2h*YmU+ z6Ux`8t)@^YQdK{ygztvX-9M(SJ3ew)oya{jB~T>NB6Iy3m-XLxyPh7gw)QMJX~PBE1Lp;x9u@dDc^BQ?)k~f6Adz!)o_Bockzb^! zxbEpH_3mYPgeUJ((k^%sPU|dbH(n(Z|47`+GUEMd^9d&xToqP0{OV82X={V7DXK^9 zh3>ZIDwb?)cer`7GUkYe>R#22l~)(Oz7(~*ODW;eC)=;x(r*ZSSqZB>{e3^?MC)!J z{h<84vrB7$w!6rWpV6=O*Dd!=_|L0rYSSvOb=O9;Ygy(j3d~Sm#uMmv&&;A$);3zi z{j}uq&p)J=J&2nA_-Lx$*yT3R(&Q*w;Xc(oZe7i%jZw9;VhWD7$9@7ZqRCb@(aCE>^1&uYHBy+ph@>A8sSg7wz719R4X3O?EyZDw}U$&YtpacR54 zsqs-?%b)iYo_}}YRMMSM&-ghbUMzIIvemsfHOQifTUF^dsP<7$AK$i5yCSeAaaNgS z$q41%$dHX)`a5`*OdX{%M&|wd?@vvOYR;sMnJn3;U9(l%cW-v@u|CxmlAgV>UyM($ zJb!QMi^p49#u|jx-aQ>Yex>jEO{bzIA{V`pu91TK;2>`0aVc>QcpPW1r|eQr0bAq;!AQ^VKPb z@2*e08RehxwsG#5iTlo7xuGWHRyfOmpl38a<(lnrW4URu`)`gK6=Np8p;Z62(8iC# zrlpU^oc*X+7RCEbL-9%B=iUXE!WFZ2)E-+~+}s@T(m?U(CiP`^K3BVy#BI3}@GSLZ z!)K!#*#+C&#hMHaPmi}zUoq$IW%GSv4epf^8yD`);8L;PT6tvlrcA+wXSTHE^?bAl zZ)j0?ky7|yW7gjK(D)vH`2~6(>k?lnH+7Z=tysMb{FE8}Hu2SSQ(xarPUp2=*8Cj< zPXNN|=5Wh^bIqHow1Qd#HiKH`;yBuI?f=IiC*^Hy_?;o9gVog@-hM$a@lQKih1ai( zzyp7tFz)Xe(I>41dPZCh5R#a5*pN6^$LRk4&vEmsU%!|Cy!lmYwzYcvGPyf3;yica zo#ye}&5T{@X(dr8BJVWY+&|x>{Gaz2;Bvc+0z=9Dhitu87fIC(gs|Z07p@olmeqc8sxP znUt)~xN%4MU#b?)s=uCZp>v~P$8nBsdEzTM*~e2Hw) zSokRlTdBAkEtu{4XU41K&5adf1>TmsedDqwyttLA>wc~N)G_n;vY9TIO-{Ql3fJc} zZd7l1$rr+<{U^5f1@Dt{G6ox`OwlMCM$z=6ReK@V;ffqU9^J->xzCjTB8$7il5+;Mls?9qL-l2y$$CQ~<< zY0cmApTo8}PNTjx__zOAzBEd4Wszp)st5T7H=8?uOIH6Fd-1~P1@m`_RpsAOSL)Gw zDcNr9R+Xwb+K+S0{l49QJ#>|KJZt!?inZg!nEN0?Kdrc-S_J2PwDX)^5Q=6AmuE)WdC}-g1=d#(wf%LH{6M~DJ4 zWeEE~sWW^ogZ3^$%CP0+;W z#BJmD+ugo@T9uC84p!}JlT%z-a3txrYfzg-$2h)^yY{d3NEg`|g|%%8}>S zlq}e)Et4`Ktmf9U>kBV?+gs057QdET9`AQWtYyF9kI0y7N1l!3p5|EWr=`T7YFlj+ z$^BR4y5sLNEwj7g4EO0S<(COF7D`StepvQ+!aClq`Y&6%_Vx%Bc2BThF5c1-8S7Dz9U2&xMs)d`A^V%~Y&P$PZqp`A6CBYgA**8=XnkomY@1>4gM6|x+;-OYGPB~j$ zDqEcQgJNcN?zr;*a?Z~kB~emyR$JsiT~_kA@_DPa2_=tPklwN^XwJEi=(T%(MISjS z`>m?IxZI(6r(gxoET`0q;r44Pr|`t{ZOQ&INw2_h$GZH73!J4s)&AHqbydQcogKMH zGy+p^>Y2?5OwFp*Pruc8cp^ z=VSy%=vum>O%uDRaLe)k0WrqG`kDfClZU>*b`TdAVOP_e%Xz)oK2! z=Df$cGc8SbZx=S{75*J{vG=5w7blEdHmJ-Hhyk?=9W<-5?CH8lTCPLZhJEKj+iU*I zEM2dG7PvK1xzSsCr?VDUxWJ#TPJ>TDN+U_xO9NMbOZr(ls?}YzwABA^B%i8D*lg$T z?ngTUg1NdEnf*_y=~u7k@{@m`ynXL{_kq*(p7LT@e1axd)L!#Q-v8Y7$ z%5x*bzgr*c>b~`iJ@6o+>{iIFkgq*8Pv;88%qXy|jU4Y;WO`3J?({)fPU-xx!e^Iv zP6~UV;8bt){R=g_C0qNxI zI>MSq_ruKp@XM^9w`Yy1dal)YHyhiLtB%p-LD(uX5{VaDkz%y z%P;D1^~3ij(%&OQR{bYt2ey?4vX4++RbuXoYU4peKq*Y``4pX(^c^vD+8 z8`d7)Io6hAIxnZ6E1C7x(&zI^-(5}nmYLp6sYsdPcK6%j`zr-<9{Gq7%`XLCz5T>j zw63yt`rDVHHGbzSP912>ZuSZ0EUtJVpQ7jf*7#oRxW+$oI?k>5{K7(~Pg*GNk$`FC zH+lJr#fwfZmvOdHxiQ6}qthnaEXvHm_7KSd>?$L{SE+uCC9F2R=G6L@u0_h%l2=Y$ZydPMs=}Io^-I0!js-D&wrL?9d|g)**L}SH z#FO`ptLU{EwU!mTHYzn;4&1SOl!~~srUyr7T}GI!8oysxV(ZlJ>z)2oPj8!Fe_iI> z+Fw7tuZ7I*x^ndElbN3nW|UifJ>REnuPSC^8~%BFRMu&;AEZ5!pl^&-G6=@3X_iIM zuacrQhsZb4n4d-eJO7fM8CAw1T|N0h%J|a)0y8#u*S%VHs;P1!$-mrwrN?5#HkZ4>(62CGrw+Z0^p?K1`c z-x=`mVc$m70)I0Y56jvNyqJ;qx^ld=+!Xr4tbr3WZvX=FV&0oV?8Qhat7&3m;^*Z< zz+9Gv9BgB06P;L;jt)q8p47=`B&wmA5-SWf~b7~AY}PrTSi>D zm$W({a>G|*<(`gxC2(gryb4K<39lV?@FH#YO_v(*f_K4i@ z)mXVveXj=22S;u!3Ox`g|MNggME=QG<*0Fi<4v;sW_~X2E`C9{D)+i@%L!{L@U2+U z1+i}>nu53Dh&}*-CpG2!p(%dy3Efc$>|y3aP@sl+e+qFjs9q&YjkP~y=cjMy2^zOh z`@R4b^LNPzh$1_+B?zV=Hx4HR9E}~BhKE+{$`R?23kD5(q-hBJdhj(ZGG)@lpy@QQ zMhv|ud^grDi6QTH=fR=Y4?^2FVPT-HEl;dQYz>_&lP6P9Fd+DC_40AUH8}p+*2r9e zuf`f2I#(RPtA`of0D}P(JgdWkK=~=N52*YSycF{5c{$j*YpUbQk9B+GTNTvh-wIw$ zP5uECJo&L8c=BUSs7*};eAEOnAuSglg1wzDfh7X7FecPSj0W$fMt}^%2m?xQh4>XU z1JL}K%mC{L`@blT_kK#hLg}|85Ef^_`-ibWfMjiSygYCt0W1>USYRW8HZ^w;E8#LR z@piZKb!K=(ns;w~y&k~9&`1j5QVbC2ap?!c(;L>Zp!u%bUuD^-G}7ODAY2l`V1a9G zQ>n0_>7*84--t|ch=Fxd(g=q8bozW4hJyn{xv-#ZV7o%))0RlZX{b2O4AcF@S4G0?nj8V6#1q)GQW9N-KFfKi4 z=nG$ur6P`fJ@F}cJzXkx-grE)K#&KeV@5~i@JuQ^2JXq=Az&RQv@uH8jE+h>K%hsZ zAB-LrWJ9^o&%mlp?HcGkL!SHOpjgkvkKkj6)AEl>Ac-B6+1R%eCyC%NA;ZYp3;^M= zz&d3-7N`e%25=}@tPb=ec>20{dEy!u_WgJ<3+lndb>u{-v zeM46g_|3d4vq`N zpdLzIkfKk9VL1ARws8&>N*Z3Cn_Zl6M~68>y9^NMjZQxp-T+~-@YoF z`FlE&&fIYsDbh(c&O;W-Hvokmqai>%R#-g9$~g%Q#879DCQXHvv6nws_;DGH;w2!f z3`D@rAk-Of&ZEz$9}JHTC01zU1vk6I1SBm-a&Tam z?G$1kz{MqL=1cH_U&o+M5Nq*dH@@)ZyW`}{ADV6aB1fMfYnX+nJV(M2+zPUsSo=>Q zHp|l?$8>rTD$^}|n+}U40dhIJ?@M7>2+sG54Y5j>e2 zM68w0M+jDf6B&GmGGwJp0>Y?r^nP2{Sm0eQfd|c3q%EePMNWf}lGLzQimxxoPgCcI zjV+;(QUAV;XsCQ2%h^z!uc@p5-( zHXV;0&t(W|^)iO}vMK;n_d$)n>IS9@mP}*0QQ=?5*xG}*4G;!)*cUd21l=hER z4A5pz0BOH$%TxD8?tm+-Gk9&pF93)kUSv#GxM7!+Hnp2;XmQi` zawM>pH^8pZZA7893=$&&GQx^7ErnWP6Tl+}Jj3k*dO6(D!V7k=eigMsO+5xlK|DN8 zSU6~PlqnGePK`&O9w*>G2bK<8cG#Q+TqP+p)e8|@3;=Kp*&@1T;PIpchGS=D?d4hv zf}A^G#aN61P9d%}pu%(Dyr=&Mib{RW!0MPBzhZ|}4Z+Sbq(^!{2v9&nI(=eCjFoAf zlRQ8;yBe$<%MmdS7|~V%JWF>VurC2t5!#3Z5#XeZ_UI2Pf?Z*t<&#w0ZAYYG+)a^lZMt6@+Nb;N=eCumlZ+MNybIMMM?-Cxi|8-&BMLp{_Suw*mLZU`BVCl(m;r1Zq- z*!=>)xT_1vLP%KzJP=AZNa=qOWfC`RWRM-iV;GKqb?+YFa*8K#aG8 z{C)krJo^6tt%bVBrAKoG;%H>;=+PVy0QAaZON57_s!{U+^=d#Jb^6@)j8lhZWar{Z z@ENiv)-|DrXrg2@OPXLyB6z%;I(Fhza)MbBk?67$6~zo3Ak$3z;+-U8^UUZLvBjAl zc`_#LGAk@9oXC_53$~i8P3@!$ZGtQ>E!c!g8}T=Qq3x&!$x+*|P%bW%ypVHKwK7-Q zyr_ruSRA}?Nd{veZNzzQ3=^Xo*kcjtk61TA#C5#`8l<{#Vqx%zp(AGQ&M+}*OD7f( zj~4={CgDM!*O1G>4Gpk>cnq;w%r;^S0A$c6A;TeTyrl*U4Y^7sq6Y}rbE`HrM^9>8 zK_v&(0c;*Wbc6sgW>8?v#agD?9eNl;gm9Jllmd;irUUgOQeIRT4b*f{B}K2W12Gcf ze*lK|m<`~`+EBAfEFjb(r5`~6)r`IA^P;0jtZC4<$^rn$1j8Y0dR9q>-ao_GdjrVJ z-UH=}*mx_2cxcNIS8^Euxst_{5a>7Xd+Mt_9@AUuvV?6w&2PYiCZX*EahOu#g3r6N zf()}+yVkVqEd{7G;K5Ff6rz0)b)uU79v(p?S52KKHg^WqN||iO2I(opQ^5@JWCa+W zwcV*OKUNPYo&%`_bUXt>sMFK}Q5@azq<6H3j_2c0hIq1!Y_9rp$vJSS@MA3H`BzF9 zb)vc+;M|05rr4Dug;=zMA*QTBLt|RH&A#0UjOTrj8bS|i6N$sA4i-(+j|G>%oKSS8q2hlCTm691c17fyy+}!VGTl&u4wI zuEMOEDa396F-Z9zNFn=pv%d)#W%x;8Y_(hp(Keix0(3q7*x-wv&`3ry$d`FguYGnO zPJ;RRW*nsv{{*1q5SHxlhs?=L7rF>08Ll_6JwZOKMvZt}G=mJ8#)XkX$&SQ1W?+_D zjG-hsHikAyXu32=;;p6FL=Z~y*#U-0vVePIp>@lAV5G(1L6?}b2WgWucky)eVuvQW z#OQ$2^I^f!a0`;PG1Fxl8T#FTz5lJd3*3fXI`0M$3c=qetjO_#K^{ZqQn6Bwq;DtNwXXUimTPVF_BgF^w)b>wXvLQof+*2^35@1BJ35XP;Toz5b#(Ktt zE&c>wTJaLS@)`8-IVhV^wya>oRo1I|)pxkN**(l1nSlKOt-z4v`Pmp()E&v^Fv?Vy z72t;CWo5m9W-T~!LA~y1aBG_uO|ZZ5@?nyu+!Tn926OZic+gGMJAh0%p5*!@Ie0i} z8stqN{{`#r7cFyB%BMk>3?>IITCj!x_PX8uEx>?hji)5ddwn=*%#|TI@Ox4t6b)yB zgBx?Cv)0@0B-=7d`tL0oq_OC-HZ0=9Bk2P%hSpmm!4Ls;5?|;THWft+_*4@#4-be)yVyp%ujh_1Yz8FrLbXg7l$4nQ- z;?zf%-o_n0^_>np3RUM!!j!dP6*YO=SDqulD+hx84r<2D0F`o72MMBE4d#e$xYcVy zp1G(Q|Egn*D68l&uxMS*xmyNUlK==T(4djCo(5r1Ug1vqax;y-bl{>3-rkOS(3sZ1 z7+*HP?Z**sOb2u;z=NiPcD!eVt`5P@k>f+ z+j%rW{u79RYubihzJ6x9{rfHEj2b)0sGb`@EI@DevW5%InZu)g-7_$@J%A|cpsqAi zAqgijJ&JgCy!Cu#I(gor4yvz(K1Ehg;V3TGeZjQ=klP6Y3^bzZZXHe#a|2dRA5 zZuD3mO?25g{gnn)EIR1M$2z{VZSsG*fOI?{jXJ4q?et008(ey;54y8e>tKp8GvFXd z9(JbyErJmIF+`VCF^Ii{Ou8$9cchJHz1Rtg-D$NE0|3Ub2|5gndY>LF8e|0b>hLG| z7s_Z=Ck;lV%1As$c-wR8j1mC`Ba9#%Jv7K-ZaCV73%wh$`6mrVG}?89^;seD9z?-% zLhnWt0u)9#QE%CS#e=L!?i2oI;1_y-_dpj7R{Hoo?XZ@Bcjk}H!?h7V0|Z7G=?CGB z5f%#)Al^c2jYNLU zpyz{~0lEeW{h?1?&&!Est3*7~PtH!LA&)Y;28oX3q%&0z(yV}sgy(*K35+=h?DWt| z1MyLugV7?gEMPF9Ck3b91Rn>2H|Xq2-N^8G?|Yl-P3DbO8ibBviZ@$QjQElest;y# z1W*FHEsGjUUjYVQ5FF;2K?^`fLpwib=5+8N)`^#_0}Uj%VKts99k45igwzw)cVNeF z0}9|#HMmcpuZGno`d&eH(EeOJ4c-00f0b(=+sSIH=Vk6$7qBEJ14;Qz~Fp2)Mj) z*H66(@7x^*^wHi8hF~{M-a8`RV4$x@fBez^2kK)_F9b(B#YPL!BR&8Q`(gxu%oDTl zu+d1T*KS*3F!$eqY!I3>cbZ9?GHg|0;NsxsY3D(phfLXa1R}2yHu+!)JP%Crp(D;E z$}n+ugn%PX{5Z-#42-w}7;!YjXaij)XpOkJ59s8^n%*I>W|PlUvc0P!_TBZQzZW(j<$_H z`N1Q8vYD|?9X0ZJRfeb!;tv@*a}op}@Ju-eNWTRSx*PLdNsD;@)y@8(jLe;ysGM~{q(>x;;Q`+L09e7%twDquBWfU9KU4t=_m;-a`1+f?wrL_Es<0-L zdSFu$1=seXc;E#N0xy8tgY0VhiqLWiBsjuRU{McPr;pl$t~OJ8U{{c(yWf9`08;`^ z4Rm`dt3zK6rY;@?xOHW2Dq_i_HmIP>6nW<00mEU}FR}i=O>H8GeaJ!V@O2i}3dmu6 zFZmoF_dYAVoeDM;+eQonz{5g!DiS~+K(_tC<9xilKAs4^I?of`!Sv4p+cR|GwOUJ; zJ86?Nhz-?2ecbWDvE#0eOn!X>X85xz2c7|7Q}JzT3hU@9VMzIK>XU4OQRQR#V;8|T z;wga3%J`E-U{r$i`SKfOf#yghV!I(-=1}b#cJ2hwN!N$@!OtkZmrwTmXlh#C zj2ZH*O95m2o(Jn=j{~oN19<(lhzYI$XHc}~J~;Iucl#P{1VjHweNNy-!hzGO_mb7J z9ua7Z1;c6?0tXeC|3)@gn3#4vx*-{y+5AwYR94&uCX_XNx(>-pU0L+X7ExuDyRxRN ze!6$3*eU^yOko{Xpm&v-mGN0*a5k};y5#9gebhkd!4=Lsy&2d>)! ztY7E?Qm~tfl3<#%zXhs+ZvUaeU=gY?J8e@bH}ry$wA4b1Tr_>WyfwYHk}kGWcg5JIbnt8q!fIBQegr!e zj&$%~W0@49?0$yyK*9(21z3^*PMVC_-5L@gls!0>a_92*0A1qXT-AjRYrN5yO?2QQ zk3PC{Ns48LH4E^@=W8wJxS8>622}+cW>FrjrN+^vY(nsZM{5S)XpMICmbe3Lc?gje zy7jJtY$V4K@Sq24F^S9&XIThna|<@PHhF{5ju+$L&_O(t(MfvrhrFR2g$zUg!rTm) z`t6lD%$36#V1h_vp2o0BhQLAO_74Pu4IEAOlrL$X2)+<4&%uFiv((Qqp{79_9g>l{ zg0TG@p^JcW815;3OjeLSqM(8+tSN{skzfSk`Z06;DiDD@1y+PMFHZ(X#<<}>$!U|0 z=Sh7)oWS)HZ&2mt0Y1Y+x^P5YCwQ@kN7TVuvI9Y*M3);3>4YQ+tUv=jN!XADI#Ay_ zu_rFTc!huy#pXd$h@m&BC}W`MTmOkikRj}K1FvlRQda_=7tI}e{t;OTXdZm?ZF)+; zNaMO9v7`tV z&|(CJVNI+5V|h5=(Fg)-p4l~gc8du_#2z+aV(u%#5KObMM18%r5f zz(6p?x(qtMPKp`=Wh2^oG3*e4 zMl5W}G3DdeV_$#;gn$qWty?+>iXP}%063xwaDg8%WL^W#a_$$&a{vvmI4hac02@Nw zoAByb8K6HE&_^c#_e%!oYxofCK#86&^XzR@ITgt4(FtHt#Tq-n@#E*5L`*q#V&#BzYhjMWc;Gm7oBQSm)5h=H`g4O}$gkotx`+o6C^4(oA- z{+hXPAjGvVi9Gyh!eK2~lo(dT5IhK_|Ak!XjXT^*L8>ZmKDf1u#ILR&m{bysl%R18 z$;h0FtS|DalM{M|h$zM1SXU9dQi(ThJN6KS4OJkbM)zY4-S7FJyz8*x64K7E6I3_3R|dBFv%@ zBdCNa&&@{Mg|4y_0Z5)=Jr z7f6$Gbc4qL(Ul}fzR-zq1}4uCHVkiurqR&fGbR&Wz>96y(WYh%?rz|2cKSEC{T<_t z2|nIlJ|qibss07K$!sG&0FboRfW?Mq+=J-!LS8(+Xn=o8gb}`=$IgHZ^bp~w!~7a{ z4M79^380*uDY}E;^sw>RVmmCfsl5l-bVpai*$aH{3f8wl7s1j1V;6}wqR}ko7^4EP z!W!Gxu1#%$7z3Q0F{*`R^BZ#wFK}5M#n{v0?q|KDFhSH$_LQUvyce6RG zYGPli;AOM)MuIKeB{1o9kwx6Xf++=|zU^Nq$rKUiyA7*K`Ztm#pAh^x9^_3#=TKVG zCtIcz^=~H}YDxcrDp-d7^UxTP>g(8X1F#6O+t#q;6IiKpKsbn9CR2!8J%$uSI_VqM zq-L<*n^gJ>s#xf>gutsg#JX!VvDM^>qZNEXWNrS1C{-grm&5y3uwOK)ipO5`RDl*c zvZjiNvBhoVZ7r$_{w++X;$Mi;VRx-825jpWgW6p5L{iF!o+>n)?L3`G@!CH!3p7u) z95g>0Hd&~d#XpfHKk3KMlfW*Dz=MrnQ-~6QG?YO)tT%CS@?^~#HXA-tCtCwL1gnA= z(Ete^gf1)#a3yZG0BDHJ0b0>e9K-1Z)1wX3OM`dk`@1w#k52I1!u<1ntH|upW4MqI zCfKtr$IBG|ou_~TO2C5+s#A!2Lg^`Ct(}{l7pY8Spiwn*DoF6Kqf(@v9ma$T*py=THU{i`?O5ch+0Qs39?}gsY z;vmvcz`*{z|A(H7uODk(V2sgae)3@pnl|d#$%qaH#30|*V_gta!L+mQfE`>0sz9p^ zM}TJLg8wW-v4{n^l(loYS15`clBKUV@eNfOEP&4*8$K&mIHpi2zzlV zg%})8UlF7@Wk3<6L<%c0(^>yJ!sLm8o^`ziZI0+Kj)O+=Po!BBVQ<_56w(Y7f~KXv zN77e_I$^Vy55WXbC?Ui#{QX)Z?R3AXbe%)~%+?kqPsU4RS=K~aCv8_kp|*bxNoE7E)P>R=&J zEO@X9#}wj=eT=CA-X~+tS(t8@zectP^lnGxex{Ycx)@x|y*eF$e#bCNcz=Mt3fOfL zd1+43_$!CiA*PhTt{|eC!6RaT8q&ant{iT$^wnVI?e1mQ?-y7zhLgvRpC{`9T{*VL zF{KB#C8<9obYd~E1=V4Z#Qu2tioo_lVkM$3Gq^N(l?L?;YZI8#1G|D0?7O((Ij{wI zScjH&Z9GC>4gGl-mICUV>t_m)-2z(Lb>S#e8el_)wo4W>ZUXXi0eN&UdHom-@}M)T zt|x3l2nst%-@IXNfoyMT(IGQO_mZ8*8DS3KWo_6I;Lh!X&B=iFeej^Pe<#wWtslg? z18%zV*$aCVE)0Abu}QB`x~66p=UGsS!}8e8CCX7&jwMQ za^AmF9h*Y7hGz5#gOj!ZK1!=oxx}XW;Gk;zc=>y?r#jT?Itu=s>evvlF4yU`@f*V`gBDr#n#^WcZy?6MCr(cuCqXGKA=);9WpJ}++|aBa6sV= zG4|Y?B_dSJgMX(t*5zCB@}c=xU~kXBR7LN1MwK$5G;$9cIdV6r!I5NsjtaKs?I1^= z7KqBGl(V9;LD^(&*pv?DXO0zd`NKj|XRsMXE7VmUv!bw}#T3dp&|#tf+fvkLC|>M( zu&b<+thP;v+QKW?RNFt15yo_l7S^>9U`;&)$}iR+GUhyGUwQ1!PBTQF3efz2SiDa+ zM#7jHsx!t#1EK%024~n>={B{JS5#Dmy+${5ok$MOK$fciushy1;^NozG(rU*TyzE! zFj^Alm?R|l8aSg_!jxI@DOJqz9#R4IiG&xKJ}EyDLY_!yR@}dyWfib0Op?pi`d#2o z=ad0Th~13fiK_(V@1)MSBs9C8H?bJB31tj;I~ zzIPtj7NW%_nazwSWUvZXj$E0@96t5Bo8C62v@s+IP{u&*CUc@x=p4$n0ICoH8#{FE zt_L?@aW{6@@jwg3T+-4^|4K0EatGoPz_c z7I@OdoKi?v=SjoEe9(8SD1)2qWYEiFoE#&WFdRfH>_jB=dV#5gj@V+E4#9M-^DqpRC( zeioG2FFHwYnuN`Epl=*)W5aXZccU%E$*MzFx8CXOsg5=2Mg!G=PFkGtoc#AeskQ(x HIgbAWP}Bnl literal 0 HcmV?d00001 diff --git a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift index f3f5ec08..a29d5038 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift @@ -17,6 +17,11 @@ final class SetNickNameView: BaseUIView { private var userNickname: String = "" public let collegeDropDownView = DropDownView(title: "단과대", items: CollegeDepartmentStore.colleges) public let departmentDropDownView = DropDownView(title: "학과", items: []) + // 저장하기 버튼 활성화 + private var isNicknameChecked = false + private var selectedCollege: String? + private var selectedDepartment: String? + // MARK: - UI Components @@ -181,11 +186,31 @@ final class SetNickNameView: BaseUIView { private func bindCollegeDepartmentDropDown() { collegeDropDownView.onSelectItem = { [weak self] college in guard let self else { return } + selectedCollege = college let departments = CollegeDepartmentStore.departments(of: college) departmentDropDownView.updateItems(departments) - departmentDropDownView.setTitle("학과") // 타이틀 초기화 + departmentDropDownView.setTitle("학과") + selectedDepartment = nil + updateCompleteButtonState() + } + + departmentDropDownView.onSelectItem = { [weak self] department in + guard let self else { return } + selectedDepartment = department + updateCompleteButtonState() } } + + private func updateCompleteButtonState() { + let isCollegeSelected = selectedCollege != nil && selectedCollege != "단과대" + let isDepartmentSelected = selectedDepartment != nil && selectedDepartment != "학과" + completeSettingNickNameButton.isEnabled = isNicknameChecked && isCollegeSelected && isDepartmentSelected + } + + public func setNicknameChecked(_ checked: Bool) { + isNicknameChecked = checked + updateCompleteButtonState() + } } // MARK: - UITextFieldDelegate @@ -234,7 +259,8 @@ private extension SetNickNameView { } func nicknameInputChanged(nickname: String) -> Bool { - completeSettingNickNameButton.isEnabled = false + isNicknameChecked = false + updateCompleteButtonState() if nickname.count > 1, nickname.count < 9 { nicknameDoubleCheckButton.isEnabled = true diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index 1fb7adaa..b3a33798 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -70,8 +70,14 @@ final class SetNickNameViewController: BaseViewController { let nickname = setNickNameView.inputNickNameTextField.text ?? "" let department = setNickNameView.departmentDropDownView.getSelectedTitle() ?? "" - setUserNickname(nickname: nickname) - setUserDepartment(department: department) + setUserNickname(nickname) { [weak self] nickOK in + guard nickOK, let self else { return } + + self.setUserDepartment(department) { deptOK in + guard deptOK else { return } + self.showCompletionAlert() + } + } } @objc @@ -133,31 +139,18 @@ final class SetNickNameViewController: BaseViewController { // MARK: - Network extension SetNickNameViewController { - private func setUserNickname(nickname: String) { - nicknameProvider.request(.setNickname(nickname: nickname)) { response in - switch response { - case .success(_): - if let currentUserInfo = UserInfoManager.shared.getCurrentUserInfo() { - UserInfoManager.shared.updateNickname(for: currentUserInfo, nickname: nickname) - } - self.showAlertController(title: "완료", message: "닉네임 설정이 완료되었습니다.", style: .cancel) { - if let myPageViewController = self.navigationController?.viewControllers.first(where: { $0 is MyPageViewController }) { - self.navigationController?.popToViewController(myPageViewController, animated: true) - } else { - let homeViewController = HomeViewController() - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) - { - keyWindow.replaceRootViewController(UINavigationController(rootViewController: homeViewController)) - } - } + private func setUserNickname(_ nickname: String, completion: @escaping (Bool) -> Void) { + nicknameProvider.request(.setNickname(nickname: nickname)) { result in + switch result { + case .success: + if let user = UserInfoManager.shared.getCurrentUserInfo() { + UserInfoManager.shared.updateNickname(for: user, nickname: nickname) } - - case let .failure(err): - print(err.localizedDescription) - + completion(true) + case .failure: RealmService.shared.resetDB() self.navigateToLogin() + completion(false) } } } @@ -172,7 +165,7 @@ extension SetNickNameViewController { if data { self.view.showToast(message: "사용 가능한 닉네임이에요") - self.setNickNameView.completeSettingNickNameButton.isEnabled = data + self.setNickNameView.setNicknameChecked(true) self.setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.nicknameTextFieldValid.hintMessage self.setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.nicknameTextFieldValid.textColor } else { @@ -197,13 +190,37 @@ extension SetNickNameViewController { } } - private func setUserDepartment(department: String) { - nicknameProvider.request(.setDepartment(department: department)) { response in - switch response { + private func setUserDepartment(_ department: String, completion: @escaping (Bool) -> Void) { + nicknameProvider.request(.setDepartment(department: department)) { result in + switch result { case .success: print("학과 등록 성공: \(department)") - case let .failure(error): + completion(true) + case .failure(let error): print("학과 등록 실패: \(error.localizedDescription)") + RealmService.shared.resetDB() + self.navigateToLogin() + completion(false) + } + } + } + + private func showCompletionAlert() { + self.showAlertController(title: "완료", + message: "설정이 완료되었습니다.", + style: .cancel) { + if let myPageVC = self.navigationController? + .viewControllers + .first(where: { $0 is MyPageViewController }) { + self.navigationController?.popToViewController(myPageVC, animated: true) + } else { + let homeVC = HomeViewController() + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) { + keyWindow.replaceRootViewController( + UINavigationController(rootViewController: homeVC) + ) + } } } } From a2ec5c9a71286ac590c8c5a0aead765774df7fad Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 7 Jul 2025 01:27:58 +0900 Subject: [PATCH 19/52] =?UTF-8?q?[#234]=20=EC=A0=9C=ED=9C=B4=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=EC=97=90=20=ED=95=99=EA=B3=BC=20=EB=9D=84=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0=20=EC=97=B0=EA=B2=B0=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/My/GetDepartmentResponse.swift | 12 ++++++++ .../Data/Network/Router/MyRouter.swift | 7 ++++- .../Network/Router/UserNicknameRouter.swift | 1 - .../Presentation/Map/View/MainMapView.swift | 28 ++++++++++------- .../MainMapViewController.swift | 30 +++++++++++++++++++ .../CustomTabBarContainerController.swift | 7 +++++ 6 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 EATSSU/App/Sources/Data/Network/DTO/My/GetDepartmentResponse.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/My/GetDepartmentResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/My/GetDepartmentResponse.swift new file mode 100644 index 00000000..f200aadb --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/My/GetDepartmentResponse.swift @@ -0,0 +1,12 @@ +// +// GetDepartmentResponse.swift +// EATSSU +// +// Created by 황상환 on 7/7/25. +// + +import Foundation + +struct GetDepartmentResponse: Codable { + let departmentName: String +} diff --git a/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift b/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift index f28fd04b..250e2594 100644 --- a/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift @@ -14,6 +14,7 @@ enum MyRouter { case myInfo case signOut case inquiry(param: InquiryRequest) + case getDepartment } extension MyRouter: TargetType { @@ -31,6 +32,8 @@ extension MyRouter: TargetType { "/users" case .inquiry: "/inquiries/" + case .getDepartment: + "/users/department" } } @@ -38,7 +41,7 @@ extension MyRouter: TargetType { switch self { case .myReview: .get - case .myInfo: + case .myInfo, .getDepartment: .get case .signOut: .delete @@ -60,6 +63,8 @@ extension MyRouter: TargetType { .requestPlain case let .inquiry(param): .requestJSONEncodable(param) + case .getDepartment: + .requestPlain } } diff --git a/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift b/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift index b09ff061..1ee43688 100644 --- a/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift @@ -12,7 +12,6 @@ enum UserNicknameRouter { case setNickname(nickname: String) case checkNickname(nickname: String) case setDepartment(department: String) -// case check } extension UserNicknameRouter: TargetType { diff --git a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift index 6c53b30b..e4257c13 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift @@ -40,16 +40,21 @@ final class MainMapView: UIView { toggleBackgroundView.backgroundColor = .white addSubview(toggleBackgroundView) - [wholeButton, myOnlyButton].forEach { - $0.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) - $0.layer.cornerRadius = 14 - $0.clipsToBounds = true - $0.backgroundColor = .clear - $0.setTitleColor(.label, for: .normal) - } - wholeButton.setTitle("전체", for: .normal) + wholeButton.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.semiBold.font(size: 14) + wholeButton.layer.cornerRadius = 14 + wholeButton.clipsToBounds = true + wholeButton.backgroundColor = .clear + wholeButton.setTitleColor(.label, for: .normal) + myOnlyButton.setTitle("내 제휴", for: .normal) + myOnlyButton.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.semiBold.font(size: 14) + myOnlyButton.setTitleColor(.label, for: .normal) + myOnlyButton.backgroundColor = .clear + myOnlyButton.layer.cornerRadius = 14 + myOnlyButton.clipsToBounds = true + myOnlyButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) + selectWhole(true) toggleBackgroundView.addSubview(wholeButton) toggleBackgroundView.addSubview(myOnlyButton) @@ -75,20 +80,21 @@ final class MainMapView: UIView { toggleBackgroundView.snp.makeConstraints { $0.top.equalTo(safeAreaLayoutGuide).offset(12) $0.centerX.equalToSuperview() - $0.width.equalTo(180) $0.height.equalTo(40) + $0.leading.greaterThanOrEqualToSuperview().offset(15) + $0.trailing.lessThanOrEqualToSuperview().inset(15) } wholeButton.snp.makeConstraints { $0.top.bottom.equalToSuperview().inset(4) $0.leading.equalToSuperview().inset(4) - $0.width.equalTo(80) + $0.width.equalTo(60) } myOnlyButton.snp.makeConstraints { $0.top.bottom.equalToSuperview().inset(4) + $0.leading.equalTo(wholeButton.snp.trailing) $0.trailing.equalToSuperview().inset(4) - $0.width.equalTo(80) } heartButton.snp.makeConstraints { diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 3bc65234..ceabb496 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -17,6 +17,8 @@ final class MainMapViewController: UIViewController { private let mainView = MainMapView() private let partnershipProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) + private let myProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) + private var markers: [NMFMarker] = [] override func loadView() { @@ -45,6 +47,7 @@ final class MainMapViewController: UIViewController { mainView.myOnlyButton.addTarget(self, action: #selector(didTapMyOnly), for: .touchUpInside) mainView.heartButton.addTarget(self, action: #selector(didTapHeart), for: .touchUpInside) + fetchDepartmentAndUpdateButton() fetchPartnerships() } @@ -62,6 +65,11 @@ final class MainMapViewController: UIViewController { print("하트 버튼 클릭됨") } + func reloadContent() { + fetchDepartmentAndUpdateButton() + fetchPartnerships() + } + private func displayMarkers(_ partnerships: [PartnershipDTO]) { markers.forEach { $0.mapView = nil } markers.removeAll() @@ -150,4 +158,26 @@ final class MainMapViewController: UIViewController { } } } + + private func fetchDepartmentAndUpdateButton() { + myProvider.request(.getDepartment) { [weak self] result in + switch result { + case .success(let response): + do { + let decoded = try response.map(BaseResponse.self) + if let department = decoded.result?.departmentName { + self?.mainView.myOnlyButton.setTitle(department, for: .normal) + } else { + self?.mainView.myOnlyButton.setTitle("내 제휴", for: .normal) + } + } catch { + print("학과 응답 디코딩 실패: \(error)") + self?.mainView.myOnlyButton.setTitle("내 제휴", for: .normal) + } + case .failure(let error): + print("학과 API 요청 실패: \(error)") + self?.mainView.myOnlyButton.setTitle("내 제휴", for: .normal) + } + } + } } diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index f0c29daf..7dbdf035 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -32,6 +32,13 @@ final class CustomTabBarContainerController: BaseViewController { self.presentLoginAlert() return } + + if index == self.currentIndex { + if let nav = self.viewControllers[index] as? UINavigationController, + let mapVC = nav.viewControllers.first as? MainMapViewController { + mapVC.reloadContent() + } + } self.switchToViewController(at: index) } From 42a857a7d3437bc7a39d58e4e927979f21cd0d1a Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 7 Jul 2025 02:54:29 +0900 Subject: [PATCH 20/52] =?UTF-8?q?[#234]=20=EC=A0=9C=ED=9C=B4=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=ED=95=99=EA=B3=BC=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20?= =?UTF-8?q?=EB=9D=84=EC=9A=B0=EA=B8=B0=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Network/Router/MyRouter.swift | 7 ++++- .../MainMapViewController.swift | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift b/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift index 250e2594..d9d46b6b 100644 --- a/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift @@ -15,6 +15,7 @@ enum MyRouter { case signOut case inquiry(param: InquiryRequest) case getDepartment + case getMyPartnerships } extension MyRouter: TargetType { @@ -34,6 +35,8 @@ extension MyRouter: TargetType { "/inquiries/" case .getDepartment: "/users/department" + case .getMyPartnerships: + "/users/department/partnerships" } } @@ -41,7 +44,7 @@ extension MyRouter: TargetType { switch self { case .myReview: .get - case .myInfo, .getDepartment: + case .myInfo, .getDepartment, .getMyPartnerships: .get case .signOut: .delete @@ -65,6 +68,8 @@ extension MyRouter: TargetType { .requestJSONEncodable(param) case .getDepartment: .requestPlain + case .getMyPartnerships: + .requestPlain } } diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index ceabb496..661823b8 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -53,12 +53,12 @@ final class MainMapViewController: UIViewController { @objc private func didTapWhole() { mainView.selectWhole(true) - print("전체 보기") + fetchPartnerships() } @objc private func didTapMyOnly() { mainView.selectWhole(false) - print("내 제휴 보기") + fetchMyPartnerships() } @objc private func didTapHeart() { @@ -141,7 +141,7 @@ final class MainMapViewController: UIViewController { } // MARK: - Network - + // 전체 제휴 private func fetchPartnerships() { partnershipProvider.request(.getAllPartnerships) { [weak self] result in switch result { @@ -158,7 +158,7 @@ final class MainMapViewController: UIViewController { } } } - + // 학과 제휴 private func fetchDepartmentAndUpdateButton() { myProvider.request(.getDepartment) { [weak self] result in switch result { @@ -180,4 +180,22 @@ final class MainMapViewController: UIViewController { } } } + + private func fetchMyPartnerships() { + myProvider.request(.getMyPartnerships) { [weak self] result in + switch result { + case .success(let response): + do { + let decoded = try response.map(BaseResponse<[PartnershipDTO]>.self) + guard let partnerships = decoded.result else { return } + self?.displayMarkers(partnerships) + } catch { + print("유저 제휴 디코딩 실패: \(error)") + } + case .failure(let error): + print("유저 제휴 네트워크 오류: \(error)") + } + } + } + } From 21d4c8ce38a61d9c0cc86d6de3e777f6bc6cae88 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:59:53 +0900 Subject: [PATCH 21/52] =?UTF-8?q?[#234]=20=ED=95=98=EB=8B=A8=20=ED=83=AD?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=8B=9C=20=EC=84=A0=ED=83=9D=ED=95=9C=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=EC=97=90=20=EB=A7=9E=EC=B6=B0=EC=84=9C=20?= =?UTF-8?q?=EC=A0=9C=ED=9C=B4=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EB=9D=84?= =?UTF-8?q?=EC=9A=B0=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/ViewController/MainMapViewController.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 661823b8..b2afd1a8 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -67,7 +67,11 @@ final class MainMapViewController: UIViewController { func reloadContent() { fetchDepartmentAndUpdateButton() - fetchPartnerships() + if mainView.wholeButton.backgroundColor == EATSSUDesignAsset.Color.Main.primary.color { + fetchPartnerships() + } else { + fetchMyPartnerships() + } } private func displayMarkers(_ partnerships: [PartnershipDTO]) { @@ -106,7 +110,9 @@ final class MainMapViewController: UIViewController { if !partnerships.isEmpty { let centerLat = latSum / Double(partnerships.count) let centerLng = lngSum / Double(partnerships.count) - let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: centerLat, lng: centerLng)) + let center = NMGLatLng(lat: centerLat, lng: centerLng) + + let cameraUpdate = NMFCameraUpdate(scrollTo: center, zoomTo: 15.5) mainView.mapView.mapView.moveCamera(cameraUpdate) } } From 18f16dd10c517a09b6f47bf1e07aa44a0969efb0 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:10:53 +0900 Subject: [PATCH 22/52] =?UTF-8?q?[#234]=20=EB=93=9C=EB=A1=AD=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=A4=91=EB=B3=B5=20=EC=83=9D=EC=84=B1=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Auth/View/DropDownView.swift | 82 ++++++++++++------- .../SetNickNameViewController.swift | 1 - 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift index f34ef0de..2a0fde95 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift @@ -19,6 +19,7 @@ final class DropDownView: UIView { public var onSelectItem: ((String) -> Void)? private var isDropdownVisible = false private var dropdownTableView: UITableView? + private static var currentlyOpenDropdown: DropDownView? // MARK: - UI Components @@ -85,39 +86,58 @@ final class DropDownView: UIView { @objc private func toggleDropdown() { guard let parentView = self.window else { return } - - if isDropdownVisible { - dropdownTableView?.removeFromSuperview() - dropdownTableView = nil - } else { - let tableView = UITableView() - tableView.layer.cornerRadius = 12 - tableView.layer.borderWidth = 1 - tableView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor - tableView.layer.shadowColor = UIColor.black.cgColor - tableView.layer.shadowOpacity = 0.1 - tableView.layer.shadowOffset = CGSize(width: 0, height: 2) - tableView.layer.shadowRadius = 4 - tableView.isScrollEnabled = true - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") - - parentView.addSubview(tableView) - - let origin = self.convert(self.bounds.origin, to: parentView) - tableView.snp.makeConstraints { - $0.top.equalTo(origin.y + 48 + 6) - $0.leading.equalTo(origin.x) - $0.width.equalTo(self.bounds.width) - $0.height.equalTo(min(items.count * 44, 200)) - } - - dropdownTableView = tableView + // 드롭메뉴 중복 생성 방지 + if DropDownView.currentlyOpenDropdown == self, isDropdownVisible { + dismissDropdown() + return + } + DropDownView.currentlyOpenDropdown?.dismissDropdown() + + let backgroundButton = UIButton() + backgroundButton.backgroundColor = .clear + backgroundButton.frame = parentView.bounds + backgroundButton.addTarget(self, action: #selector(dismissDropdown), for: .touchUpInside) + backgroundButton.tag = 999 + parentView.addSubview(backgroundButton) + + // 드롭메뉴 생성 + let tableView = UITableView() + tableView.layer.cornerRadius = 12 + tableView.layer.borderWidth = 1 + tableView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + tableView.layer.shadowColor = UIColor.black.cgColor + tableView.layer.shadowOpacity = 0.1 + tableView.layer.shadowOffset = CGSize(width: 0, height: 2) + tableView.layer.shadowRadius = 4 + tableView.isScrollEnabled = true + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + parentView.addSubview(tableView) + + let origin = self.convert(self.bounds.origin, to: parentView) + tableView.snp.makeConstraints { + $0.top.equalTo(origin.y + 48 + 6) + $0.leading.equalTo(origin.x) + $0.width.equalTo(self.bounds.width) + $0.height.equalTo(min(items.count * 44, 200)) } - isDropdownVisible.toggle() + dropdownTableView = tableView + isDropdownVisible = true + DropDownView.currentlyOpenDropdown = self + } + // 드롭메뉴 중복 생성 방지 + @objc public func dismissDropdown() { + dropdownTableView?.superview?.viewWithTag(999)?.removeFromSuperview() + dropdownTableView?.removeFromSuperview() + dropdownTableView = nil + isDropdownVisible = false + + if DropDownView.currentlyOpenDropdown == self { + DropDownView.currentlyOpenDropdown = nil + } } // MARK: - Public diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index b3a33798..f54e7bdc 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -26,7 +26,6 @@ final class SetNickNameViewController: BaseViewController { } } - // MARK: - Life Cycles override func viewDidLoad() { From 45e855e6eb5cbf4ec75721fe55d9a0eb032cd868 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 8 Jul 2025 01:03:40 +0900 Subject: [PATCH 23/52] =?UTF-8?q?[#234]=20merge=20=EB=B2=84=EA=B7=B8=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 --- .../Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift | 2 -- .../Presentation/Home/ViewController/HomeViewController.swift | 1 - 2 files changed, 3 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift b/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift index 29874c2c..7b5e525f 100644 --- a/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift +++ b/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift @@ -7,8 +7,6 @@ import Foundation -// Sources/Data/CollegeDepartmentStore.swift - struct CollegeDepartment { let college: String let departments: [String] diff --git a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift index d46cab22..3aedf29c 100644 --- a/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift +++ b/EATSSU/App/Sources/Presentation/Home/ViewController/HomeViewController.swift @@ -42,7 +42,6 @@ final class HomeViewController: BaseViewController { configureUI() setLayout() registerTabman() - setupNavigationBar() // 이미 등록된 옵저버가 있으면 먼저 제거 NotificationCenter.default.removeObserver(self, name: .didEnterNewDay, object: nil) From ee82096e81c1ca28bb564cc4fd7081aafa1722ed Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:46:18 +0900 Subject: [PATCH 24/52] =?UTF-8?q?[#234]=20=EC=9E=90=EC=8B=A0=EC=9D=98=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=B0=8D=EA=B8=B0=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Map/View/MainMapView.swift | 2 ++ .../MainMapViewController.swift | 19 +++++++++++++++++-- EATSSU/Project.swift | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift index e4257c13..6ab7e5d6 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift @@ -32,6 +32,8 @@ final class MainMapView: UIView { backgroundColor = .white mapView.showZoomControls = false + mapView.showLocationButton = true + mapView.mapView.positionMode = .direction addSubview(mapView) toggleBackgroundView.layer.cornerRadius = 20 diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index b2afd1a8..e634db87 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -12,9 +12,10 @@ import Moya import EATSSUDesign -final class MainMapViewController: UIViewController { +final class MainMapViewController: UIViewController, CLLocationManagerDelegate { private let mainView = MainMapView() + private let locationManager = CLLocationManager() private let partnershipProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) private let myProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) @@ -27,7 +28,9 @@ final class MainMapViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() + title = "제휴 지도" // 배경색 채우기 @@ -73,6 +76,18 @@ final class MainMapViewController: UIViewController { fetchMyPartnerships() } } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + switch manager.authorizationStatus { + case .authorizedWhenInUse, .authorizedAlways: + if let location = manager.location?.coordinate { + let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: location.latitude, lng: location.longitude), zoomTo: 15.5) + mainView.mapView.mapView.moveCamera(cameraUpdate) + } + default: + break + } + } private func displayMarkers(_ partnerships: [PartnershipDTO]) { markers.forEach { $0.mapView = nil } diff --git a/EATSSU/Project.swift b/EATSSU/Project.swift index 9ab1a7aa..186b627f 100644 --- a/EATSSU/Project.swift +++ b/EATSSU/Project.swift @@ -16,6 +16,7 @@ let appInfoPlist: InfoPlist = .extendingDefault(with: [ "kakaoplus", "kakaotalk", ], + "NSLocationWhenInUseUsageDescription": "내 위치를 확인하기 위해 권한이 필요합니다.", "UIApplicationSceneManifest": [ "UIApplicationSupportsMultipleScenes": false, "UISceneConfigurations": [ From 193cda6a095ce84119538ad9c5d6f0725c0f3c48 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:01:20 +0900 Subject: [PATCH 25/52] =?UTF-8?q?[#234]=20=ED=95=99=EA=B3=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=95=88=ED=96=88=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EB=9C=A8=EB=8A=94=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainMapViewController.swift | 28 ++++-- .../NoDepartmentSheetViewController.swift | 96 +++++++++++++++++++ 2 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index e634db87..7c3ffe28 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -16,6 +16,7 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { private let mainView = MainMapView() private let locationManager = CLLocationManager() + private var currentDepartmentName: String? private let partnershipProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) private let myProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) @@ -50,7 +51,7 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { mainView.myOnlyButton.addTarget(self, action: #selector(didTapMyOnly), for: .touchUpInside) mainView.heartButton.addTarget(self, action: #selector(didTapHeart), for: .touchUpInside) - fetchDepartmentAndUpdateButton() +// fetchDepartmentAndUpdateButton() fetchPartnerships() } @@ -60,8 +61,18 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { } @objc private func didTapMyOnly() { + if currentDepartmentName?.isEmpty ?? true { + presentNoDepartmentSheet() + return + } + mainView.selectWhole(false) - fetchMyPartnerships() +// fetchMyPartnerships() + } + + private func presentNoDepartmentSheet() { + let sheetVC = NoDepartmentSheetViewController() + present(sheetVC, animated: true) } @objc private func didTapHeart() { @@ -73,7 +84,7 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { if mainView.wholeButton.backgroundColor == EATSSUDesignAsset.Color.Main.primary.color { fetchPartnerships() } else { - fetchMyPartnerships() +// fetchMyPartnerships() } } @@ -186,17 +197,18 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { case .success(let response): do { let decoded = try response.map(BaseResponse.self) - if let department = decoded.result?.departmentName { - self?.mainView.myOnlyButton.setTitle(department, for: .normal) - } else { - self?.mainView.myOnlyButton.setTitle("내 제휴", for: .normal) - } + let department = decoded.result?.departmentName ?? "" + self?.currentDepartmentName = department // 저장 + let buttonTitle = department.isEmpty ? "내 제휴" : department + self?.mainView.myOnlyButton.setTitle(buttonTitle, for: .normal) } catch { print("학과 응답 디코딩 실패: \(error)") + self?.currentDepartmentName = nil self?.mainView.myOnlyButton.setTitle("내 제휴", for: .normal) } case .failure(let error): print("학과 API 요청 실패: \(error)") + self?.currentDepartmentName = nil self?.mainView.myOnlyButton.setTitle("내 제휴", for: .normal) } } diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift new file mode 100644 index 00000000..cf74ebc0 --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift @@ -0,0 +1,96 @@ +// +// NoDepartmentSheetViewController.swift +// EATSSU +// +// Created by 황상환 on 7/15/25. +// + +import UIKit +import SnapKit +import EATSSUDesign + +final class NoDepartmentSheetViewController: UIViewController { + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "학과를 입력하고\n나만의 제휴를 확인해보세요!" + label.numberOfLines = 2 + label.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 18) + label.textColor = .label + return label + }() + + private let logoImageView = UIImageView(image: EATSSUDesignAsset.Images.authLogo.image) + private let subTitleImageView = UIImageView(image: EATSSUDesignAsset.Images.authSubTitle.image) + + private lazy var logoStackView: UIStackView = { + logoImageView.contentMode = .scaleAspectFit + subTitleImageView.contentMode = .scaleAspectFit + subTitleImageView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + + let stack = UIStackView(arrangedSubviews: [logoImageView, subTitleImageView]) + stack.axis = .vertical + stack.alignment = .center + stack.spacing = 4 + return stack + }() + + private let inputButton: ESButton = { + let button = ESButton(size: .big, title: "학과 입력하기") + button.isEnabled = true + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + setupSheet() + setupLayout() + inputButton.addTarget(self, action: #selector(goToDepartmentSetting), for: .touchUpInside) + } + + private func setupSheet() { + view.backgroundColor = .white + view.layer.cornerRadius = 20 + view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + if let sheet = sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = true + } + } + + private func setupLayout() { + view.addSubview(titleLabel) + view.addSubview(logoStackView) + view.addSubview(inputButton) + + titleLabel.snp.makeConstraints { + $0.top.equalTo(view.safeAreaLayoutGuide).offset(40) + $0.leading.trailing.equalToSuperview().inset(24) + } + + logoStackView.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(90) + $0.centerX.equalToSuperview() + $0.width.equalTo(160) + $0.height.equalTo(70) + } + + inputButton.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(24) + $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(26) + $0.height.equalTo(52) + } + } + + @objc private func goToDepartmentSetting() { + let vc = SetNickNameViewController() + if let nav = presentingViewController as? UINavigationController { + dismiss(animated: true) { + nav.pushViewController(vc, animated: true) + } + } else { + dismiss(animated: true) + } + } +} From a529720d60216804e8d7650415cd5c046f765fb4 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:19:39 +0900 Subject: [PATCH 26/52] =?UTF-8?q?[#234]=20=EB=B0=94=ED=85=80=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=EC=97=90=EC=84=9C=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80-=EB=82=B4=20=EC=A0=95=EB=B3=B4=EB=A1=9C=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NoDepartmentSheetViewController.swift | 19 ++++++++++++++----- .../CustomTabBarContainerController.swift | 9 +++++++++ .../Sources/Utility/Literal/TextLiteral.swift | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift index cf74ebc0..9b0fdce5 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift @@ -70,7 +70,7 @@ final class NoDepartmentSheetViewController: UIViewController { } logoStackView.snp.makeConstraints { - $0.top.equalTo(titleLabel.snp.bottom).offset(90) + $0.top.equalTo(titleLabel.snp.bottom).offset(100) $0.centerX.equalToSuperview() $0.width.equalTo(160) $0.height.equalTo(70) @@ -78,16 +78,25 @@ final class NoDepartmentSheetViewController: UIViewController { inputButton.snp.makeConstraints { $0.leading.trailing.equalToSuperview().inset(24) - $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(26) + $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(5) $0.height.equalTo(52) } } @objc private func goToDepartmentSetting() { - let vc = SetNickNameViewController() - if let nav = presentingViewController as? UINavigationController { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let sceneDelegate = windowScene.delegate as? SceneDelegate, + let tabContainer = sceneDelegate.window?.rootViewController as? CustomTabBarContainerController else { + dismiss(animated: true) + return + } + + // 탭 전환 + tabContainer.setTab(index: 2) + + if let myNav = tabContainer.getNavController(at: 2) { dismiss(animated: true) { - nav.pushViewController(vc, animated: true) + myNav.pushViewController(SetNickNameViewController(), animated: true) } } else { dismiss(animated: true) diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 7dbdf035..a9aea222 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -96,4 +96,13 @@ final class CustomTabBarContainerController: BaseViewController { window.replaceRootViewController(loginVC) } } + + public func setTab(index: Int) { + switchToViewController(at: index) + } + + public func getNavController(at index: Int) -> UINavigationController? { + guard index < viewControllers.count else { return nil } + return viewControllers[index] as? UINavigationController + } } diff --git a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift index 430cdbcf..a33ce766 100644 --- a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift +++ b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift @@ -36,7 +36,7 @@ enum TextLiteral { static let signInWithApple: String = "Apple로 로그인" static let signInWithKakao: String = "카카오 로그인" static let lookingWithNoSignIn: String = "둘러보기" - static let setNickName: String = "닉네임 설정" + static let setNickName: String = "내 정보" static let nickNameLabel: String = "닉네임" static let inputNickName: String = "닉네임을 입력해주세요" static let inputNickNameLabel: String = "닉네임을 설정해 주세요." From cbe4e961c8d14a99ac82516a774e624f3daec46d Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 17 Jul 2025 01:38:52 +0900 Subject: [PATCH 27/52] =?UTF-8?q?[#234]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=B0=EA=B2=B0=EB=90=9C=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Auth/View/SetNickNameView.swift | 75 ++++++++++++------- .../SetNickNameViewController.swift | 5 +- .../MyPage/View/MyPageView/MyPageView.swift | 53 +------------ 3 files changed, 50 insertions(+), 83 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift index a29d5038..d90d96eb 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift @@ -17,15 +17,12 @@ final class SetNickNameView: BaseUIView { private var userNickname: String = "" public let collegeDropDownView = DropDownView(title: "단과대", items: CollegeDepartmentStore.colleges) public let departmentDropDownView = DropDownView(title: "학과", items: []) - // 저장하기 버튼 활성화 private var isNicknameChecked = false private var selectedCollege: String? private var selectedDepartment: String? - // MARK: - UI Components - // 닉네임 설정 private let nickNameLabel: UILabel = { let label = UILabel() label.text = "닉네임 설정" @@ -61,8 +58,7 @@ final class SetNickNameView: BaseUIView { stackView.spacing = 8.0 return stackView }() - - // 소속 설정 + private let affiliationLabel: UILabel = { let label = UILabel() label.text = "소속 설정" @@ -80,7 +76,6 @@ final class SetNickNameView: BaseUIView { return stackView }() - // 연결 계정 private let connectedAccountLabel: UILabel = { let label = UILabel() label.text = "연결된 계정" @@ -88,20 +83,41 @@ final class SetNickNameView: BaseUIView { return label }() - public let connectedProviderLabel: UILabel = { + private let accountTypeLabel: UILabel = { let label = UILabel() - label.text = "APPLE" - label.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) + label.text = "없음" + label.font = .bold(size: 14) return label }() - private let appleIconImageView: UIImageView = { + private let accountTypeImage: UIImageView = { let imageView = UIImageView() - imageView.image = EATSSUDesignAsset.Images.signWithApple.image imageView.contentMode = .scaleAspectFit + imageView.snp.makeConstraints { + $0.width.height.equalTo(20) + } return imageView }() + private lazy var accountStackView: UIStackView = { + let stack = UIStackView(arrangedSubviews: [accountTypeLabel, accountTypeImage]) + stack.axis = .horizontal + stack.alignment = .bottom + stack.spacing = 5 + return stack + }() + + private lazy var totalAccountStackView: UIStackView = { + let stack = UIStackView(arrangedSubviews: [ + connectedAccountLabel, + accountStackView + ]) + stack.axis = .horizontal + stack.alignment = .bottom + stack.spacing = 20 + return stack + }() + public var completeSettingNickNameButton: ESButton = { let button = ESButton(size: .big, title: "저장하기") button.isEnabled = false @@ -125,9 +141,7 @@ final class SetNickNameView: BaseUIView { nicknameDoubleCheckButton, affiliationLabel, affiliationStackView, - connectedAccountLabel, - connectedProviderLabel, - appleIconImageView, + totalAccountStackView, completeSettingNickNameButton ) } @@ -158,21 +172,11 @@ final class SetNickNameView: BaseUIView { collegeDropDownView.snp.makeConstraints { $0.height.equalTo(48) } departmentDropDownView.snp.makeConstraints { $0.height.equalTo(48) } - connectedAccountLabel.snp.makeConstraints { + totalAccountStackView.snp.makeConstraints { $0.top.equalTo(affiliationStackView.snp.bottom).offset(40) - $0.leading.equalToSuperview().inset(24) - } - - connectedProviderLabel.snp.makeConstraints { - $0.centerY.equalTo(connectedAccountLabel) - $0.trailing.equalTo(appleIconImageView.snp.leading).offset(-4) + $0.leading.trailing.equalToSuperview().inset(24) } - appleIconImageView.snp.makeConstraints { - $0.centerY.equalTo(connectedAccountLabel) - $0.trailing.equalToSuperview().inset(24) - $0.width.height.equalTo(20) - } completeSettingNickNameButton.snp.makeConstraints { $0.horizontalEdges.equalToSuperview().inset(24) $0.bottom.equalTo(safeAreaLayoutGuide).inset(26) @@ -182,7 +186,7 @@ final class SetNickNameView: BaseUIView { func setTextFieldDelegate() { inputNickNameTextField.delegate = self } - + private func bindCollegeDepartmentDropDown() { collegeDropDownView.onSelectItem = { [weak self] college in guard let self else { return } @@ -200,17 +204,30 @@ final class SetNickNameView: BaseUIView { updateCompleteButtonState() } } - + private func updateCompleteButtonState() { let isCollegeSelected = selectedCollege != nil && selectedCollege != "단과대" let isDepartmentSelected = selectedDepartment != nil && selectedDepartment != "학과" completeSettingNickNameButton.isEnabled = isNicknameChecked && isCollegeSelected && isDepartmentSelected } - + public func setNicknameChecked(_ checked: Bool) { isNicknameChecked = checked updateCompleteButtonState() } + + public func setAccountInfo() { + if let accountType = UserInfoManager.shared.getCurrentUserInfo()?.accountType { + switch accountType { + case .apple: + accountTypeLabel.text = "APPLE" + accountTypeImage.image = EATSSUDesignAsset.Images.signWithApple.image + case .kakao: + accountTypeLabel.text = "카카오" + accountTypeImage.image = EATSSUDesignAsset.Images.signWithKakao.image + } + } + } } // MARK: - UITextFieldDelegate diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index f54e7bdc..64e02047 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -34,8 +34,9 @@ final class SetNickNameViewController: BaseViewController { dismissKeyboard() } - override func viewWillAppear(_: Bool) { - addKeyboardNotifications() + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setNickNameView.setAccountInfo() } override func viewWillDisappear(_: Bool) { diff --git a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift index 9b9db2db..69afdd58 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift @@ -38,41 +38,6 @@ final class MyPageView: BaseUIView { return button }() - // "연결된 계정" 레이블 - let accountTitleLabel: UILabel = { - let label = UILabel() - label.text = TextLiteral.MyPage.linkedAccount - label.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 14) - return label - }() - - // 서버에서 계정 정보를 가져오기 전 기본값 - var accountTypeLabel: UILabel = { - let label = UILabel() - label.text = "없음" - label.font = .bold(size: 14) - return label - }() - - // 소셜 로그인 공급업체 아이콘 - var accountTypeImage = UIImageView() - - lazy var accountStackView: UIStackView = { - let stack = UIStackView(arrangedSubviews: [accountTypeLabel, accountTypeImage]) - stack.axis = .horizontal - stack.alignment = .bottom - stack.spacing = 5 - return stack - }() - - lazy var totalAccountStackView: UIStackView = { - let stack = UIStackView(arrangedSubviews: [accountTitleLabel, accountStackView]) - stack.axis = .horizontal - stack.alignment = .bottom - stack.spacing = 20 - return stack - }() - let myPageTableView: UITableView = { let tableView = UITableView() tableView.separatorStyle = .none @@ -133,7 +98,6 @@ final class MyPageView: BaseUIView { contentView.addSubviews( userImage, userNicknameButton, - totalAccountStackView, myPageTableView, appVersionStringLabel, appVersionLabel, @@ -164,13 +128,8 @@ final class MyPageView: BaseUIView { $0.height.equalTo(40) } - totalAccountStackView.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.top.equalTo(userNicknameButton.snp.bottom).offset(10) - } - myPageTableView.snp.makeConstraints { - $0.top.equalTo(accountTitleLabel.snp.bottom).offset(24) + $0.top.equalTo(userNicknameButton.snp.bottom).offset(16) $0.leading.trailing.equalToSuperview() $0.height.equalTo(420) $0.width.equalToSuperview() @@ -217,15 +176,5 @@ final class MyPageView: BaseUIView { titleColor: .black, fontName: .semiBold(size: 20) ) - if let accountType = UserInfoManager.shared.getCurrentUserInfo()?.accountType { - switch accountType { - case .apple: - accountTypeLabel.text = "APPLE" - accountTypeImage.image = EATSSUDesignAsset.Images.signWithApple.image - case .kakao: - accountTypeLabel.text = "카카오" - accountTypeImage.image = EATSSUDesignAsset.Images.signWithKakao.image - } - } } } From dd6b8409a9064b20500185bed8273b708cd8145e Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 21 Jul 2025 00:42:26 +0900 Subject: [PATCH 28/52] =?UTF-8?q?[#234]=20=EB=82=B4=20=EC=A0=9C=ED=9C=B4?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=20=EB=B0=8F=20=ED=95=99=EA=B3=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B7=B0=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/ViewController/MainMapViewController.swift | 6 +++--- .../ViewController/NoDepartmentSheetViewController.swift | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 7c3ffe28..86cbc72d 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -51,7 +51,7 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { mainView.myOnlyButton.addTarget(self, action: #selector(didTapMyOnly), for: .touchUpInside) mainView.heartButton.addTarget(self, action: #selector(didTapHeart), for: .touchUpInside) -// fetchDepartmentAndUpdateButton() + fetchDepartmentAndUpdateButton() fetchPartnerships() } @@ -67,7 +67,7 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { } mainView.selectWhole(false) -// fetchMyPartnerships() + fetchMyPartnerships() } private func presentNoDepartmentSheet() { @@ -84,7 +84,7 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { if mainView.wholeButton.backgroundColor == EATSSUDesignAsset.Color.Main.primary.color { fetchPartnerships() } else { -// fetchMyPartnerships() + fetchMyPartnerships() } } diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift index 9b0fdce5..21768e86 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift @@ -79,7 +79,6 @@ final class NoDepartmentSheetViewController: UIViewController { inputButton.snp.makeConstraints { $0.leading.trailing.equalToSuperview().inset(24) $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(5) - $0.height.equalTo(52) } } From 893e1bc95805b837853119e7c6897ac2d5c03292 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 21 Jul 2025 02:02:07 +0900 Subject: [PATCH 29/52] =?UTF-8?q?[#234]=20=EC=A0=9C=ED=9C=B4=20=EB=A7=88?= =?UTF-8?q?=EC=BB=A4=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Map/View/MapMarkerView.swift | 63 +++++++++++++------ .../MainMapViewController.swift | 25 ++------ 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift index fdd112a0..a308cc03 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift @@ -6,24 +6,14 @@ // import UIKit - import EATSSUDesign final class MapMarkerView: UIView { - private let iconImageView = UIImageView() private let titleLabel = UILabel() init(icon: UIImage?, title: String) { super.init(frame: .zero) - setupUI(icon: icon, title: title) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupUI(icon: UIImage?, title: String) { backgroundColor = .white layer.cornerRadius = 13 layer.masksToBounds = true @@ -32,21 +22,55 @@ final class MapMarkerView: UIView { iconImageView.image = icon iconImageView.contentMode = .scaleAspectFit - iconImageView.snp.makeConstraints { $0.width.equalTo(20) } + addSubview(iconImageView) titleLabel.text = title titleLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 10) titleLabel.textColor = .black + addSubview(titleLabel) + } - let hStack = UIStackView(arrangedSubviews: [iconImageView, titleLabel]) - hStack.axis = .horizontal - hStack.alignment = .center - hStack.spacing = 2 - hStack.layoutMargins = UIEdgeInsets(top: 3, left: 2, bottom: 3, right: 8) - hStack.isLayoutMarginsRelativeArrangement = true + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - addSubview(hStack) - hStack.snp.makeConstraints { $0.edges.equalToSuperview() } + override var intrinsicContentSize: CGSize { + let labelSize = titleLabel.intrinsicContentSize + let leftPadding: CGFloat = 3 + let iconSize: CGFloat = 20 + let textSpacing: CGFloat = 3 + let rightPadding: CGFloat = 7 + let height: CGFloat = 25 + + let width = leftPadding + + iconSize + + textSpacing + + labelSize.width + + rightPadding + return CGSize(width: width, height: height) + } + + override func layoutSubviews() { + super.layoutSubviews() + let h = bounds.height + let leftRightPadding: CGFloat = 3 + let iconSize: CGFloat = 20 + let textSpacing: CGFloat = 3 + + iconImageView.frame = CGRect( + x: leftRightPadding, + y: (h - iconSize) / 2, + width: iconSize, + height: iconSize + ) + + let labelSize = titleLabel.intrinsicContentSize + titleLabel.frame = CGRect( + x: iconImageView.frame.maxX + textSpacing, + y: (h - labelSize.height) / 2, + width: labelSize.width, + height: labelSize.height + ) } func toImage() -> UIImage { @@ -56,5 +80,4 @@ final class MapMarkerView: UIView { layer.render(in: ctx.cgContext) } } - } diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 86cbc72d..065edb90 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -145,31 +145,18 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { private func makeMarkerImage(type: String, title: String) -> UIImage { let icon: UIImage? - switch type { - case "RESTAURANT": - icon = EATSSUDesignAsset.Images.restaurantPin.image - case "CAFE": - icon = EATSSUDesignAsset.Images.cafePin.image - case "PUB": - icon = EATSSUDesignAsset.Images.pubPin.image - default: - icon = EATSSUDesignAsset.Images.restaurantPin.image + case "RESTAURANT": icon = EATSSUDesignAsset.Images.restaurantPin.image + case "CAFE": icon = EATSSUDesignAsset.Images.cafePin.image + case "PUB": icon = EATSSUDesignAsset.Images.pubPin.image + default: icon = EATSSUDesignAsset.Images.restaurantPin.image } let markerView = MapMarkerView(icon: icon, title: title) - markerView.setNeedsLayout() markerView.layoutIfNeeded() - - let fittingSize = markerView.systemLayoutSizeFitting( - CGSize(width: UIView.layoutFittingCompressedSize.width, height: 24), - withHorizontalFittingPriority: .fittingSizeLevel, - verticalFittingPriority: .required - ) - - markerView.frame = CGRect(origin: .zero, size: fittingSize) + let size = markerView.intrinsicContentSize + markerView.frame = CGRect(origin: .zero, size: size) return markerView.toImage() - } // MARK: - Network From 14801c3f49a15d60f91f89b02a0efd027bd2a346 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:33:26 +0900 Subject: [PATCH 30/52] =?UTF-8?q?[#234]=20=EC=B0=9C=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Map/View/MainMapView.swift | 22 +------------------ .../MainMapViewController.swift | 6 ----- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift index 6ab7e5d6..55401107 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift @@ -16,8 +16,7 @@ final class MainMapView: UIView { let toggleBackgroundView = UIView() let wholeButton = UIButton(type: .system) let myOnlyButton = UIButton(type: .system) - let heartButton = UIButton(type: .system) - + override init(frame: CGRect) { super.init(frame: frame) setup() @@ -60,18 +59,6 @@ final class MainMapView: UIView { selectWhole(true) toggleBackgroundView.addSubview(wholeButton) toggleBackgroundView.addSubview(myOnlyButton) - - let heartImage = UIImage(systemName: "heart")? - .withConfiguration(UIImage.SymbolConfiguration(pointSize: 13, weight: .regular)) - heartButton.setImage(heartImage, for: .normal) - heartButton.tintColor = EATSSUDesignAsset.Color.Red.error.color - heartButton.backgroundColor = .white - heartButton.layer.cornerRadius = 20 - heartButton.layer.shadowColor = UIColor.black.cgColor - heartButton.layer.shadowOpacity = 0.1 - heartButton.layer.shadowRadius = 4 - heartButton.layer.shadowOffset = CGSize(width: 0, height: 2) - addSubview(heartButton) } private func setLayout() { @@ -98,15 +85,8 @@ final class MainMapView: UIView { $0.leading.equalTo(wholeButton.snp.trailing) $0.trailing.equalToSuperview().inset(4) } - - heartButton.snp.makeConstraints { - $0.trailing.equalToSuperview().inset(16) - $0.top.equalTo(safeAreaLayoutGuide).offset(12) - $0.size.equalTo(40) - } } - func selectWhole(_ isSelected: Bool) { if isSelected { wholeButton.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 065edb90..9e2ba9b5 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -34,7 +34,6 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { title = "제휴 지도" - // 배경색 채우기 let navBarAppearance = UINavigationBarAppearance() navBarAppearance.configureWithOpaqueBackground() navBarAppearance.backgroundColor = .white @@ -49,7 +48,6 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { mainView.wholeButton.addTarget(self, action: #selector(didTapWhole), for: .touchUpInside) mainView.myOnlyButton.addTarget(self, action: #selector(didTapMyOnly), for: .touchUpInside) - mainView.heartButton.addTarget(self, action: #selector(didTapHeart), for: .touchUpInside) fetchDepartmentAndUpdateButton() fetchPartnerships() @@ -74,10 +72,6 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { let sheetVC = NoDepartmentSheetViewController() present(sheetVC, animated: true) } - - @objc private func didTapHeart() { - print("하트 버튼 클릭됨") - } func reloadContent() { fetchDepartmentAndUpdateButton() From fd5511340c824522a8103c19a4c1faa59d1eb209 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:40:36 +0900 Subject: [PATCH 31/52] =?UTF-8?q?[#234]=20=ED=95=99=EA=B3=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=94=EB=A1=9C=20=EB=B0=98=EC=98=81=EC=95=88?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/ViewController/MainMapViewController.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 9e2ba9b5..635161fc 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -52,6 +52,12 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { fetchDepartmentAndUpdateButton() fetchPartnerships() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + fetchDepartmentAndUpdateButton() + } @objc private func didTapWhole() { mainView.selectWhole(true) From da6562d78cd8ead795097d4d9affc720db48a879 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:37:45 +0900 Subject: [PATCH 32/52] =?UTF-8?q?[#234]=20=EC=A0=9C=ED=9C=B4=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EA=B4=80=EB=A0=A8=20=ED=8C=8C=EC=9D=BC=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EB=B0=8F=20MARK=20=EC=A0=95=EB=A6=AC,=20BaseView?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Auth/View/DropDownView.swift | 106 ++++++------ .../Presentation/Map/View/MainMapView.swift | 33 ++-- .../Presentation/Map/View/MapMarkerView.swift | 44 +++-- .../MainMapViewController.swift | 153 +++++++++++------- .../NoDepartmentSheetViewController.swift | 31 ++-- ...PartnershipDetailSheetViewController.swift | 43 +++-- .../CustomTabBarContainerController.swift | 44 +++-- .../TabBar/CustomTabBarView.swift | 17 +- 8 files changed, 301 insertions(+), 170 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift index 2a0fde95..740789fb 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift @@ -7,11 +7,11 @@ import UIKit -import EATSSUDesign - import SnapKit -final class DropDownView: UIView { +import EATSSUDesign + +final class DropDownView: BaseUIView { // MARK: - Properties @@ -31,40 +31,21 @@ final class DropDownView: UIView { init(title: String, items: [String]) { self.items = items super.init(frame: .zero) - setupButton(title: title) - setupLayout() - setupAction() } required init?(coder: NSCoder) { fatalError() } - // MARK: - Setup + // MARK: - View Setup - private func setupButton(title: String) { - var config = UIButton.Configuration.filled() - config.attributedTitle = AttributedString( - title, - attributes: AttributeContainer([ - .font: EATSSUDesignFontFamily.Pretendard.regular.font(size: 14), - .foregroundColor: EATSSUDesignAsset.Color.GrayScale.gray700.color - ]) - ) - config.baseBackgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color - config.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 32) - config.titleAlignment = .leading - - button.configuration = config - button.layer.cornerRadius = 12 - button.layer.borderWidth = 1 - button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor - button.contentHorizontalAlignment = .leading - } - - private func setupLayout() { + override func configureUI() { + configureButton(title: getTitleString()) + arrow.tintColor = EATSSUDesignAsset.Color.GrayScale.gray700.color addSubviews(button, arrow) + } + override func setLayout() { button.snp.makeConstraints { $0.edges.equalToSuperview() } @@ -74,33 +55,36 @@ final class DropDownView: UIView { $0.trailing.equalToSuperview().inset(12) $0.width.height.equalTo(16) } - - arrow.tintColor = EATSSUDesignAsset.Color.GrayScale.gray700.color } - private func setupAction() { + func setButtonEvent() { button.addTarget(self, action: #selector(toggleDropdown), for: .touchUpInside) } - // MARK: - Dropdown + // MARK: - Dropdown Control + /// 드롭다운 토글 처리 @objc private func toggleDropdown() { guard let parentView = self.window else { return } - // 드롭메뉴 중복 생성 방지 + + // 이미 열려있는 드롭다운이면 닫음 if DropDownView.currentlyOpenDropdown == self, isDropdownVisible { dismissDropdown() return } + + // 다른 드롭다운이 열려있으면 닫기 DropDownView.currentlyOpenDropdown?.dismissDropdown() + // 배경 탭 시 닫히도록 투명 버튼 추가 let backgroundButton = UIButton() backgroundButton.backgroundColor = .clear backgroundButton.frame = parentView.bounds backgroundButton.addTarget(self, action: #selector(dismissDropdown), for: .touchUpInside) backgroundButton.tag = 999 parentView.addSubview(backgroundButton) - - // 드롭메뉴 생성 + + // 드롭다운 테이블 생성 let tableView = UITableView() tableView.layer.cornerRadius = 12 tableView.layer.borderWidth = 1 @@ -116,6 +100,7 @@ final class DropDownView: UIView { tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") parentView.addSubview(tableView) + // 위치 계산 let origin = self.convert(self.bounds.origin, to: parentView) tableView.snp.makeConstraints { $0.top.equalTo(origin.y + 48 + 6) @@ -128,7 +113,8 @@ final class DropDownView: UIView { isDropdownVisible = true DropDownView.currentlyOpenDropdown = self } - // 드롭메뉴 중복 생성 방지 + + /// 드롭다운 닫기 @objc public func dismissDropdown() { dropdownTableView?.superview?.viewWithTag(999)?.removeFromSuperview() dropdownTableView?.removeFromSuperview() @@ -140,31 +126,53 @@ final class DropDownView: UIView { } } - // MARK: - Public + // MARK: - Public Functions + /// 드롭다운 항목 갱신 public func updateItems(_ newItems: [String]) { items = newItems dropdownTableView?.reloadData() } + /// 버튼 타이틀 설정 public func setTitle(_ title: String) { - var config = button.configuration - config?.attributedTitle = AttributedString( + configureButton(title: title) + } + + /// 현재 선택된 타이틀 반환 + public func getSelectedTitle() -> String? { + if let attributed = button.configuration?.attributedTitle { + return NSAttributedString(attributed).string + } + return nil + } + + // MARK: - Private Helpers + + /// 버튼 스타일 설정 + private func configureButton(title: String) { + var config = UIButton.Configuration.filled() + config.attributedTitle = AttributedString( title, attributes: AttributeContainer([ .font: EATSSUDesignFontFamily.Pretendard.regular.font(size: 14), .foregroundColor: EATSSUDesignAsset.Color.GrayScale.gray700.color ]) ) - config?.titleAlignment = .leading + config.baseBackgroundColor = EATSSUDesignAsset.Color.GrayScale.gray100.color + config.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 32) + config.titleAlignment = .leading + button.configuration = config + button.layer.cornerRadius = 12 + button.layer.borderWidth = 1 + button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor + button.contentHorizontalAlignment = .leading } - - public func getSelectedTitle() -> String? { - if let attributed = button.configuration?.attributedTitle { - return NSAttributedString(attributed).string - } - return nil + + /// 최초 타이틀 반환 + private func getTitleString() -> String { + return items.first ?? "선택" } } @@ -188,8 +196,8 @@ extension DropDownView: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let selected = items[indexPath.row] - setTitle(selected) - onSelectItem?(selected) - toggleDropdown() + setTitle(selected) + onSelectItem?(selected) + toggleDropdown() } } diff --git a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift index 55401107..8b06048d 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift @@ -6,41 +6,40 @@ // import UIKit + import NMapsMap import SnapKit + import EATSSUDesign -final class MainMapView: UIView { +final class MainMapView: BaseUIView { + + // MARK: - UI Components let mapView = NMFNaverMapView() let toggleBackgroundView = UIView() let wholeButton = UIButton(type: .system) let myOnlyButton = UIButton(type: .system) - - override init(frame: CGRect) { - super.init(frame: frame) - setup() - setLayout() - } - - required init?(coder: NSCoder) { - fatalError() - } - private func setup() { + // MARK: - UI Setup + + override func configureUI() { backgroundColor = .white + // 네이버 지도 뷰 설정 mapView.showZoomControls = false mapView.showLocationButton = true mapView.mapView.positionMode = .direction addSubview(mapView) + // 상단 버튼 배경 뷰 설정 toggleBackgroundView.layer.cornerRadius = 20 toggleBackgroundView.layer.borderWidth = 1 toggleBackgroundView.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor toggleBackgroundView.backgroundColor = .white addSubview(toggleBackgroundView) + // 전체 버튼 wholeButton.setTitle("전체", for: .normal) wholeButton.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.semiBold.font(size: 14) wholeButton.layer.cornerRadius = 14 @@ -48,6 +47,7 @@ final class MainMapView: UIView { wholeButton.backgroundColor = .clear wholeButton.setTitleColor(.label, for: .normal) + // 내 제휴 버튼 myOnlyButton.setTitle("내 제휴", for: .normal) myOnlyButton.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.semiBold.font(size: 14) myOnlyButton.setTitleColor(.label, for: .normal) @@ -56,12 +56,16 @@ final class MainMapView: UIView { myOnlyButton.clipsToBounds = true myOnlyButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) + // 초기 선택 상태 설정 selectWhole(true) + toggleBackgroundView.addSubview(wholeButton) toggleBackgroundView.addSubview(myOnlyButton) } - private func setLayout() { + // MARK: - Layout Setup + + override func setLayout() { mapView.snp.makeConstraints { $0.edges.equalToSuperview() } @@ -87,6 +91,8 @@ final class MainMapView: UIView { } } + // MARK: - Button Selection Handling + func selectWhole(_ isSelected: Bool) { if isSelected { wholeButton.backgroundColor = EATSSUDesignAsset.Color.Main.primary.color @@ -102,5 +108,4 @@ final class MainMapView: UIView { myOnlyButton.setTitleColor(.white, for: .normal) } } - } diff --git a/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift index a308cc03..346ada3b 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MapMarkerView.swift @@ -6,33 +6,52 @@ // import UIKit + import EATSSUDesign -final class MapMarkerView: UIView { +final class MapMarkerView: BaseUIView { + + // MARK: - UI Components + private let iconImageView = UIImageView() private let titleLabel = UILabel() + // MARK: - Init + init(icon: UIImage?, title: String) { super.init(frame: .zero) + setup(icon: icon, title: title) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View Setup + + override func configureUI() { backgroundColor = .white layer.cornerRadius = 13 layer.masksToBounds = true layer.borderWidth = 1 layer.borderColor = UIColor.systemGray3.cgColor - iconImageView.image = icon iconImageView.contentMode = .scaleAspectFit addSubview(iconImageView) + addSubview(titleLabel) + } + + // MARK: - Setup + + private func setup(icon: UIImage?, title: String) { + iconImageView.image = icon titleLabel.text = title titleLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 10) titleLabel.textColor = .black - addSubview(titleLabel) } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + // MARK: - Layout override var intrinsicContentSize: CGSize { let labelSize = titleLabel.intrinsicContentSize @@ -42,20 +61,17 @@ final class MapMarkerView: UIView { let rightPadding: CGFloat = 7 let height: CGFloat = 25 - let width = leftPadding - + iconSize - + textSpacing - + labelSize.width - + rightPadding + let width = leftPadding + iconSize + textSpacing + labelSize.width + rightPadding return CGSize(width: width, height: height) } override func layoutSubviews() { super.layoutSubviews() + let h = bounds.height let leftRightPadding: CGFloat = 3 - let iconSize: CGFloat = 20 - let textSpacing: CGFloat = 3 + let iconSize: CGFloat = 20 + let textSpacing: CGFloat = 3 iconImageView.frame = CGRect( x: leftRightPadding, @@ -73,6 +89,8 @@ final class MapMarkerView: UIView { ) } + // MARK: - Convert View to Image + func toImage() -> UIImage { layoutIfNeeded() let renderer = UIGraphicsImageRenderer(size: bounds.size) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 635161fc..1d81ef79 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -12,110 +12,143 @@ import Moya import EATSSUDesign -final class MainMapViewController: UIViewController, CLLocationManagerDelegate { +final class MainMapViewController: BaseViewController, CLLocationManagerDelegate { - private let mainView = MainMapView() + // MARK: - Properties + + private let root = MainMapView() private let locationManager = CLLocationManager() private var currentDepartmentName: String? - - private let partnershipProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) - private let myProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) + + private let partnershipProvider = MoyaProvider( + session: Session(interceptor: AuthInterceptor.shared) + ) + private let myProvider = MoyaProvider( + session: Session(interceptor: AuthInterceptor.shared) + ) private var markers: [NMFMarker] = [] - override func loadView() { - self.view = mainView + // MARK: - View Setup + + override func configureUI() { + view.addSubview(root) + } + + override func setLayout() { + root.snp.makeConstraints { $0.edges.equalToSuperview() } + } + + override func setButtonEvent() { + root.wholeButton.addTarget(self, action: #selector(didTapWhole), for: .touchUpInside) + root.myOnlyButton.addTarget(self, action: #selector(didTapMyOnly), for: .touchUpInside) } + // MARK: - Life Cycle + override func viewDidLoad() { super.viewDidLoad() + + // 위치 권한 요청 locationManager.delegate = self locationManager.requestWhenInUseAuthorization() - - title = "제휴 지도" + // 네비게이션 바 스타일 설정 + title = "제휴 지도" let navBarAppearance = UINavigationBarAppearance() navBarAppearance.configureWithOpaqueBackground() - navBarAppearance.backgroundColor = .white + navBarAppearance.backgroundColor = .white navBarAppearance.titleTextAttributes = [ .foregroundColor: UIColor.black, .font: EATSSUDesignFontFamily.Pretendard.bold.font(size: 16) ] - navigationController?.navigationBar.standardAppearance = navBarAppearance navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance navigationController?.navigationBar.compactAppearance = navBarAppearance - mainView.wholeButton.addTarget(self, action: #selector(didTapWhole), for: .touchUpInside) - mainView.myOnlyButton.addTarget(self, action: #selector(didTapMyOnly), for: .touchUpInside) - + // 초기 데이터 로드 fetchDepartmentAndUpdateButton() fetchPartnerships() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + // 화면 진입 시 학과명 다시 확인 fetchDepartmentAndUpdateButton() } + // MARK: - Action + + /// "전체" 버튼 탭 시 호출 @objc private func didTapWhole() { - mainView.selectWhole(true) + root.selectWhole(true) fetchPartnerships() } + /// "내 제휴" 버튼 탭 시 호출 @objc private func didTapMyOnly() { - if currentDepartmentName?.isEmpty ?? true { + guard !(currentDepartmentName?.isEmpty ?? true) else { presentNoDepartmentSheet() return } - - mainView.selectWhole(false) + root.selectWhole(false) fetchMyPartnerships() } - + + /// 학과 미선택 시 바텀시트 표시 private func presentNoDepartmentSheet() { let sheetVC = NoDepartmentSheetViewController() present(sheetVC, animated: true) } - + + /// 외부에서 콘텐츠 새로고침 요청할 때 사용 func reloadContent() { fetchDepartmentAndUpdateButton() - if mainView.wholeButton.backgroundColor == EATSSUDesignAsset.Color.Main.primary.color { + if root.wholeButton.backgroundColor == EATSSUDesignAsset.Color.Main.primary.color { fetchPartnerships() } else { fetchMyPartnerships() } } - + + // MARK: - Location Delegate + + /// 위치 권한 변경 시 지도 카메라 이동 func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch manager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways: if let location = manager.location?.coordinate { - let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: location.latitude, lng: location.longitude), zoomTo: 15.5) - mainView.mapView.mapView.moveCamera(cameraUpdate) + let cameraUpdate = NMFCameraUpdate( + scrollTo: NMGLatLng(lat: location.latitude, lng: location.longitude), + zoomTo: 15.5 + ) + root.mapView.mapView.moveCamera(cameraUpdate) } default: break } } + // MARK: - Marker Display + + /// 제휴 목록을 지도 마커로 표시 private func displayMarkers(_ partnerships: [PartnershipDTO]) { markers.forEach { $0.mapView = nil } markers.removeAll() - var latSum: Double = 0 - var lngSum: Double = 0 + var latSum = 0.0 + var lngSum = 0.0 for partnership in partnerships { let marker = NMFMarker() marker.position = NMGLatLng(lat: partnership.latitude, lng: partnership.longitude) - + let markerImage = makeMarkerImage(type: partnership.restaurantType, title: partnership.storeName) marker.iconImage = NMFOverlayImage(image: markerImage) marker.width = CGFloat(UInt32(markerImage.size.width)) marker.height = CGFloat(UInt32(markerImage.size.height)) - + marker.touchHandler = { [weak self] _ in let vc = PartnershipDetailSheetViewController( storeName: partnership.storeName, @@ -125,8 +158,8 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { self?.present(vc, animated: true) return true } - - marker.mapView = mainView.mapView.mapView + + marker.mapView = root.mapView.mapView markers.append(marker) latSum += partnership.latitude @@ -136,31 +169,34 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { if !partnerships.isEmpty { let centerLat = latSum / Double(partnerships.count) let centerLng = lngSum / Double(partnerships.count) - let center = NMGLatLng(lat: centerLat, lng: centerLng) - - let cameraUpdate = NMFCameraUpdate(scrollTo: center, zoomTo: 15.5) - mainView.mapView.mapView.moveCamera(cameraUpdate) + let cameraUpdate = NMFCameraUpdate( + scrollTo: NMGLatLng(lat: centerLat, lng: centerLng), + zoomTo: 15.5 + ) + root.mapView.mapView.moveCamera(cameraUpdate) } } + /// 마커에 들어갈 커스텀 이미지 생성 private func makeMarkerImage(type: String, title: String) -> UIImage { - let icon: UIImage? - switch type { - case "RESTAURANT": icon = EATSSUDesignAsset.Images.restaurantPin.image - case "CAFE": icon = EATSSUDesignAsset.Images.cafePin.image - case "PUB": icon = EATSSUDesignAsset.Images.pubPin.image - default: icon = EATSSUDesignAsset.Images.restaurantPin.image - } + let icon: UIImage? = { + switch type { + case "RESTAURANT": return EATSSUDesignAsset.Images.restaurantPin.image + case "CAFE": return EATSSUDesignAsset.Images.cafePin.image + case "PUB": return EATSSUDesignAsset.Images.pubPin.image + default: return EATSSUDesignAsset.Images.restaurantPin.image + } + }() let markerView = MapMarkerView(icon: icon, title: title) markerView.layoutIfNeeded() - let size = markerView.intrinsicContentSize - markerView.frame = CGRect(origin: .zero, size: size) + markerView.frame = CGRect(origin: .zero, size: markerView.intrinsicContentSize) return markerView.toImage() } // MARK: - Network - // 전체 제휴 + + /// 전체 제휴 데이터 조회 private func fetchPartnerships() { partnershipProvider.request(.getAllPartnerships) { [weak self] result in switch result { @@ -170,14 +206,15 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { guard let partnerships = decoded.result else { return } self?.displayMarkers(partnerships) } catch { - print("Decoding 실패: \(error.localizedDescription)") + print("전체 제휴 디코딩 실패: \(error.localizedDescription)") } case .failure(let error): - print("네트워크 오류: \(error.localizedDescription)") + print("전체 제휴 네트워크 오류: \(error.localizedDescription)") } } } - // 학과 제휴 + + /// 유저 학과 조회 및 버튼 타이틀 업데이트 private func fetchDepartmentAndUpdateButton() { myProvider.request(.getDepartment) { [weak self] result in switch result { @@ -185,22 +222,23 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { do { let decoded = try response.map(BaseResponse.self) let department = decoded.result?.departmentName ?? "" - self?.currentDepartmentName = department // 저장 + self?.currentDepartmentName = department let buttonTitle = department.isEmpty ? "내 제휴" : department - self?.mainView.myOnlyButton.setTitle(buttonTitle, for: .normal) + self?.root.myOnlyButton.setTitle(buttonTitle, for: .normal) } catch { - print("학과 응답 디코딩 실패: \(error)") + print("학과 디코딩 실패: \(error)") self?.currentDepartmentName = nil - self?.mainView.myOnlyButton.setTitle("내 제휴", for: .normal) + self?.root.myOnlyButton.setTitle("내 제휴", for: .normal) } case .failure(let error): - print("학과 API 요청 실패: \(error)") + print("학과 API 실패: \(error)") self?.currentDepartmentName = nil - self?.mainView.myOnlyButton.setTitle("내 제휴", for: .normal) + self?.root.myOnlyButton.setTitle("내 제휴", for: .normal) } } } - + + /// 내 제휴 데이터 조회 private func fetchMyPartnerships() { myProvider.request(.getMyPartnerships) { [weak self] result in switch result { @@ -210,12 +248,11 @@ final class MainMapViewController: UIViewController, CLLocationManagerDelegate { guard let partnerships = decoded.result else { return } self?.displayMarkers(partnerships) } catch { - print("유저 제휴 디코딩 실패: \(error)") + print("내 제휴 디코딩 실패: \(error)") } case .failure(let error): - print("유저 제휴 네트워크 오류: \(error)") + print("내 제휴 네트워크 오류: \(error)") } } } - } diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift index 21768e86..acfd2e9e 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift @@ -6,10 +6,14 @@ // import UIKit + import SnapKit + import EATSSUDesign -final class NoDepartmentSheetViewController: UIViewController { +final class NoDepartmentSheetViewController: BaseViewController { + + // MARK: - UI Components private let titleLabel: UILabel = { let label = UILabel() @@ -41,14 +45,10 @@ final class NoDepartmentSheetViewController: UIViewController { return button }() - override func viewDidLoad() { - super.viewDidLoad() - setupSheet() - setupLayout() - inputButton.addTarget(self, action: #selector(goToDepartmentSetting), for: .touchUpInside) - } + // MARK: - View Setup - private func setupSheet() { + override func configureUI() { + // 바텀시트 배경 및 모서리 설정 view.backgroundColor = .white view.layer.cornerRadius = 20 view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] @@ -57,12 +57,13 @@ final class NoDepartmentSheetViewController: UIViewController { sheet.detents = [.medium()] sheet.prefersGrabberVisible = true } - } - private func setupLayout() { view.addSubview(titleLabel) view.addSubview(logoStackView) view.addSubview(inputButton) + } + + override func setLayout() { titleLabel.snp.makeConstraints { $0.top.equalTo(view.safeAreaLayoutGuide).offset(40) @@ -82,6 +83,13 @@ final class NoDepartmentSheetViewController: UIViewController { } } + override func setButtonEvent() { + inputButton.addTarget(self, action: #selector(goToDepartmentSetting), for: .touchUpInside) + } + + // MARK: - Navigation + + /// "학과 입력하기" 버튼 클릭 시 학과 설정 화면으로 이동 @objc private func goToDepartmentSetting() { guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let sceneDelegate = windowScene.delegate as? SceneDelegate, @@ -90,10 +98,11 @@ final class NoDepartmentSheetViewController: UIViewController { return } - // 탭 전환 + // "내 정보" 탭으로 전환 tabContainer.setTab(index: 2) if let myNav = tabContainer.getNavController(at: 2) { + // 닉네임/학과 설정 화면으로 푸시 dismiss(animated: true) { myNav.pushViewController(SetNickNameViewController(), animated: true) } diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 1630d522..df9a7b2e 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -6,15 +6,29 @@ // import UIKit + import SnapKit + import EATSSUDesign -final class PartnershipDetailSheetViewController: UIViewController { +final class PartnershipDetailSheetViewController: BaseViewController { + + // MARK: - Properties private let storeName: String private let restaurantType: String private let partnershipInfos: [PartnershipInfoDTO] + // MARK: - UI Components + + private let storeNameLabel = UILabel() + private let typeStackView = UIStackView() + private let typeIconImageView = UIImageView() + private let typeTextLabel = UILabel() + private let infoListStackView = UIStackView() + + // MARK: - Init + init(storeName: String, restaurantType: String, partnershipInfos: [PartnershipInfoDTO]) { self.storeName = storeName self.restaurantType = restaurantType @@ -32,21 +46,11 @@ final class PartnershipDetailSheetViewController: UIViewController { fatalError() } - private let storeNameLabel = UILabel() - private let typeStackView = UIStackView() - private let typeIconImageView = UIImageView() - private let typeTextLabel = UILabel() - private let infoListStackView = UIStackView() + // MARK: - View Setup - override func viewDidLoad() { - super.viewDidLoad() + override func configureUI() { view.backgroundColor = .white - setupViews() - setupLayout() - configureData() - } - private func setupViews() { storeNameLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 16) storeNameLabel.textColor = .label @@ -74,7 +78,7 @@ final class PartnershipDetailSheetViewController: UIViewController { } } - private func setupLayout() { + override func setLayout() { storeNameLabel.snp.makeConstraints { $0.top.equalTo(view.safeAreaLayoutGuide).offset(20) $0.leading.trailing.equalToSuperview().inset(24) @@ -91,6 +95,14 @@ final class PartnershipDetailSheetViewController: UIViewController { } } + // MARK: - Data Config + + override func viewDidLoad() { + super.viewDidLoad() + configureData() + } + + /// 매장 정보와 제휴 내용을 화면에 반영 private func configureData() { storeNameLabel.text = storeName @@ -116,6 +128,9 @@ final class PartnershipDetailSheetViewController: UIViewController { } } + // MARK: - UI Helpers + + /// 제휴 정보 카드 뷰 생성 private func makeInfoCard(info: PartnershipInfoDTO, isLast: Bool) -> UIView { let collegeName = info.collegeName ?? info.departmentName let start = String(info.startDate.dropFirst(5)) diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index a9aea222..afaba139 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -9,6 +9,8 @@ import UIKit final class CustomTabBarContainerController: BaseViewController { + // MARK: - Properties + private let tabBarView = CustomTabBarView() private let viewControllers: [UIViewController] = [ UINavigationController(rootViewController: HomeViewController()), @@ -17,10 +19,7 @@ final class CustomTabBarContainerController: BaseViewController { ] private var currentIndex = 0 - override func viewDidLoad() { - super.viewDidLoad() - switchToViewController(at: currentIndex) - } + // MARK: - View Setup override func configureUI() { view.addSubview(tabBarView) @@ -28,11 +27,13 @@ final class CustomTabBarContainerController: BaseViewController { tabBarView.buttonTapped = { [weak self] index in guard let self = self else { return } + // 마이페이지는 로그인 필요 if index == 2, RealmService.shared.isAccessTokenPresent() == false { self.presentLoginAlert() return } - + + // 같은 탭 다시 클릭 시 reloadContent if index == self.currentIndex { if let nav = self.viewControllers[index] as? UINavigationController, let mapVC = nav.viewControllers.first as? MainMapViewController { @@ -52,14 +53,26 @@ final class CustomTabBarContainerController: BaseViewController { } } + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + switchToViewController(at: currentIndex) + } + + // MARK: - Navigation Control + + /// 탭 전환 처리 private func switchToViewController(at index: Int) { let selectedVC = viewControllers[index] + // 기존 자식 뷰컨 정리 children.forEach { child in child.view.removeFromSuperview() child.removeFromParent() } + // 새로운 뷰컨 추가 addChild(selectedVC) view.insertSubview(selectedVC.view, belowSubview: tabBarView) selectedVC.view.snp.makeConstraints { @@ -72,10 +85,14 @@ final class CustomTabBarContainerController: BaseViewController { currentIndex = index } + /// 로그인 필요 시 알림창 표시 private func presentLoginAlert() { - let alert = UIAlertController(title: "로그인이 필요한 서비스입니다", - message: "로그인 하시겠습니까?", - preferredStyle: .alert) + let alert = UIAlertController( + title: "로그인이 필요한 서비스입니다", + message: "로그인 하시겠습니까?", + preferredStyle: .alert + ) + let confirmAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in self?.navigateToLogin() } @@ -83,24 +100,31 @@ final class CustomTabBarContainerController: BaseViewController { guard let self = self else { return } self.tabBarView.setSelectedIndex(self.currentIndex) } + alert.addAction(confirmAction) alert.addAction(cancelAction) present(alert, animated: true) } + /// 로그인 화면으로 전환 private func navigateToLogin() { let loginVC = LoginViewController() + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let sceneDelegate = windowScene.delegate as? SceneDelegate, let window = sceneDelegate.window { window.replaceRootViewController(loginVC) } } - + + // MARK: - Public Interface + + /// 외부에서 탭 전환 요청 시 사용 public func setTab(index: Int) { switchToViewController(at: index) } - + + /// 특정 탭의 네비게이션 컨트롤러 반환 public func getNavController(at index: Int) -> UINavigationController? { guard index < viewControllers.count else { return nil } return viewControllers[index] as? UINavigationController diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift index 79ddf887..563d57e6 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift @@ -11,6 +11,8 @@ import EATSSUDesign final class CustomTabBarView: BaseUIView { + // MARK: - Properties + var buttonTapped: ((Int) -> Void)? private let buttons: [UIButton] = { @@ -42,7 +44,10 @@ final class CustomTabBarView: BaseUIView { } }() + // MARK: - View Setup + override func configureUI() { + // 배경 및 그림자 설정 backgroundColor = .white layer.cornerRadius = 10 layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] @@ -53,6 +58,7 @@ final class CustomTabBarView: BaseUIView { layer.shadowOffset = CGSize(width: 0, height: -3) layer.shadowRadius = 12 + // 각 버튼 액션 연결 buttons.forEach { button in button.setTitleColor(.gray, for: .normal) button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) @@ -60,15 +66,21 @@ final class CustomTabBarView: BaseUIView { } override func setLayout() { + // 버튼을 수평 스택뷰로 배치 let stack = UIStackView(arrangedSubviews: buttons) stack.axis = .horizontal stack.distribution = .fillEqually stack.alignment = .center addSubview(stack) - stack.snp.makeConstraints { $0.edges.equalToSuperview().inset(8) } + stack.snp.makeConstraints { + $0.edges.equalToSuperview().inset(8) + } } + // MARK: - Public Functions + + /// 선택된 인덱스의 버튼 스타일 업데이트 func setSelectedIndex(_ index: Int) { for (i, button) in buttons.enumerated() { let isSelected = i == index @@ -86,6 +98,9 @@ final class CustomTabBarView: BaseUIView { } } + // MARK: - Actions + + /// 버튼 클릭 시 인덱스 변경 및 콜백 호출 @objc private func buttonTapped(_ sender: UIButton) { setSelectedIndex(sender.tag) buttonTapped?(sender.tag) From 5435f2290113ea571e9f109a1402cd773a5332b5 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:07:05 +0900 Subject: [PATCH 33/52] =?UTF-8?q?[#234]=20=EB=82=B4=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=20=ED=99=9C=EC=84=B1=ED=99=94=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=20=EB=B0=8F=20=EB=8B=A8=EA=B3=BC=EB=8C=80=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Presentation/Auth/View/DropDownView.swift | 10 ++++------ .../Sources/Presentation/Map/View/MainMapView.swift | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift index 740789fb..87a9e739 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/DropDownView.swift @@ -20,6 +20,7 @@ final class DropDownView: BaseUIView { private var isDropdownVisible = false private var dropdownTableView: UITableView? private static var currentlyOpenDropdown: DropDownView? + private let placeholderTitle: String // MARK: - UI Components @@ -29,8 +30,10 @@ final class DropDownView: BaseUIView { // MARK: - Init init(title: String, items: [String]) { + self.placeholderTitle = title self.items = items super.init(frame: .zero) + setButtonEvent() } required init?(coder: NSCoder) { @@ -40,7 +43,7 @@ final class DropDownView: BaseUIView { // MARK: - View Setup override func configureUI() { - configureButton(title: getTitleString()) + configureButton(title: placeholderTitle) arrow.tintColor = EATSSUDesignAsset.Color.GrayScale.gray700.color addSubviews(button, arrow) } @@ -169,11 +172,6 @@ final class DropDownView: BaseUIView { button.layer.borderColor = EATSSUDesignAsset.Color.GrayScale.gray300.color.cgColor button.contentHorizontalAlignment = .leading } - - /// 최초 타이틀 반환 - private func getTitleString() -> String { - return items.first ?? "선택" - } } // MARK: - UITableViewDelegate, UITableViewDataSource diff --git a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift index 8b06048d..11ed6cb9 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift @@ -29,7 +29,7 @@ final class MainMapView: BaseUIView { // 네이버 지도 뷰 설정 mapView.showZoomControls = false mapView.showLocationButton = true - mapView.mapView.positionMode = .direction + mapView.mapView.positionMode = .disabled addSubview(mapView) // 상단 버튼 배경 뷰 설정 From 5df4d1d67e00f2dfa7d97c6ce9b2668d0924d74b Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:21:56 +0900 Subject: [PATCH 34/52] =?UTF-8?q?[#234]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=8B=9C=20=EB=9C=A8=EB=8A=94=20=EB=A9=98=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Auth/ViewController/SetNickNameViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index 64e02047..012c072b 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -207,7 +207,7 @@ extension SetNickNameViewController { private func showCompletionAlert() { self.showAlertController(title: "완료", - message: "설정이 완료되었습니다.", + message: "정보 수정이 완료되었습니다.", style: .cancel) { if let myPageVC = self.navigationController? .viewControllers From fea5ae5ef19692a25035926dd5a34996635589db Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:34:05 +0900 Subject: [PATCH 35/52] =?UTF-8?q?[#234]=20=EC=A0=9C=ED=9C=B4=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=ED=83=AD=EC=9C=BC=EB=A1=9C=20=EB=B3=B5=EA=B7=80?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=84=EC=B2=B4=20=EB=B2=84=ED=8A=BC=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=84=B8=ED=8C=85=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/ViewController/MainMapViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index 1d81ef79..bb203a33 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -74,8 +74,10 @@ final class MainMapViewController: BaseViewController, CLLocationManagerDelegate override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - // 화면 진입 시 학과명 다시 확인 + // 항상 '전체' 버튼이 선택된 상태로 초기화 + root.selectWhole(true) fetchDepartmentAndUpdateButton() + fetchPartnerships() } // MARK: - Action From 5d8adc3c1a453c0f83f420971a9f58a6948d1673 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 24 Jul 2025 01:19:40 +0900 Subject: [PATCH 36/52] =?UTF-8?q?[#234]=20=EC=A0=9C=ED=9C=B4=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20dto=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Network/DTO/Partnership/PartnershipDTO.swift | 2 +- .../PartnershipDetailSheetViewController.swift | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift index 6b65069c..e33e2b9d 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift @@ -19,7 +19,7 @@ struct PartnershipInfoDTO: Codable { let id: Int let partnershipType: String let collegeName: String? - let departmentName: String + let departmentName: String? let likeCount: Int let isLiked: Bool let description: String diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index df9a7b2e..5c6bfe31 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -132,15 +132,15 @@ final class PartnershipDetailSheetViewController: BaseViewController { /// 제휴 정보 카드 뷰 생성 private func makeInfoCard(info: PartnershipInfoDTO, isLast: Bool) -> UIView { - let collegeName = info.collegeName ?? info.departmentName + let labelText = info.collegeName ?? info.departmentName ?? "학과 정보 없음" + let start = String(info.startDate.dropFirst(5)) let end = String(info.endDate.dropFirst(5)) - let titleDateLabel = UILabel() - let fullText = "\(collegeName) \(start) ~ \(end)" + let fullText = "\(labelText) \(start) ~ \(end)" let attrText = NSMutableAttributedString(string: fullText) - let collegeRange = (fullText as NSString).range(of: collegeName) + let collegeRange = (fullText as NSString).range(of: labelText) let dateRange = (fullText as NSString).range(of: "\(start) ~ \(end)") attrText.addAttributes([ @@ -154,6 +154,7 @@ final class PartnershipDetailSheetViewController: BaseViewController { .baselineOffset: +2 ], range: dateRange) + let titleDateLabel = UILabel() titleDateLabel.attributedText = attrText let descriptionLabel = UILabel() @@ -185,4 +186,5 @@ final class PartnershipDetailSheetViewController: BaseViewController { return paddedContainer } + } From a938a7f9958aba70fc18471c39fbb0e5ca9e2240 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:33:47 +0900 Subject: [PATCH 37/52] =?UTF-8?q?[#234]=20=EB=8B=A8=EA=B3=BC=EB=8C=80/?= =?UTF-8?q?=ED=95=99=EA=B3=BC=20API=20=EC=97=B0=EA=B2=B0=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Network/DTO/My/LookupItemDTO.swift | 16 +++++ .../Data/Network/Router/MyRouter.swift | 31 ++++++---- .../Auth/Data/CollegeDepartmentStore.swift | 34 ----------- .../Auth/View/SetNickNameView.swift | 20 ++++++- .../SetNickNameViewController.swift | 60 ++++++++++++++++--- 5 files changed, 106 insertions(+), 55 deletions(-) create mode 100644 EATSSU/App/Sources/Data/Network/DTO/My/LookupItemDTO.swift delete mode 100644 EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift diff --git a/EATSSU/App/Sources/Data/Network/DTO/My/LookupItemDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/My/LookupItemDTO.swift new file mode 100644 index 00000000..340e36d4 --- /dev/null +++ b/EATSSU/App/Sources/Data/Network/DTO/My/LookupItemDTO.swift @@ -0,0 +1,16 @@ +// +// LookupItemDTO.swift +// EATSSU +// +// Created by 황상환 on 8/13/25. +// + +import Foundation + +struct LookupItemDTO: Codable { + let id: Int + let name: String +} + +typealias CollegesResponseDTO = BaseResponse<[LookupItemDTO]> +typealias DepartmentsResponseDTO = BaseResponse<[LookupItemDTO]> diff --git a/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift b/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift index d9d46b6b..646176a4 100644 --- a/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/MyRouter.swift @@ -16,6 +16,8 @@ enum MyRouter { case inquiry(param: InquiryRequest) case getDepartment case getMyPartnerships + case colleges + case departments(collegeId: Int) } extension MyRouter: TargetType { @@ -37,12 +39,16 @@ extension MyRouter: TargetType { "/users/department" case .getMyPartnerships: "/users/department/partnerships" + case .colleges: + "/users/lookup/colleges" + case .departments: + "/users/lookup/departments" } } var method: Moya.Method { switch self { - case .myReview: + case .myReview, .departments, .colleges: .get case .myInfo, .getDepartment, .getMyPartnerships: .get @@ -56,20 +62,25 @@ extension MyRouter: TargetType { var task: Moya.Task { switch self { case .myReview: - .requestParameters(parameters: ["page": 0, - "size": 20, - "sort": "date,DESC"], - encoding: URLEncoding.queryString) + .requestParameters(parameters: ["page": 0, + "size": 20, + "sort": "date,DESC"], + encoding: URLEncoding.queryString) case .myInfo: - .requestPlain + .requestPlain case .signOut: - .requestPlain + .requestPlain case let .inquiry(param): - .requestJSONEncodable(param) + .requestJSONEncodable(param) case .getDepartment: - .requestPlain + .requestPlain case .getMyPartnerships: - .requestPlain + .requestPlain + case .colleges: + .requestPlain + case let .departments(collegeId): + .requestParameters(parameters: ["collegeId": collegeId], + encoding: URLEncoding.queryString) } } diff --git a/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift b/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift deleted file mode 100644 index 7b5e525f..00000000 --- a/EATSSU/App/Sources/Presentation/Auth/Data/CollegeDepartmentStore.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// CollegeDepartmentStore.swift -// EATSSU-DEV -// -// Created by 황상환 on 6/26/25. -// - -import Foundation - -struct CollegeDepartment { - let college: String - let departments: [String] -} - -enum CollegeDepartmentStore { - static let list: [CollegeDepartment] = [ - .init(college: "IT대학", departments: ["컴퓨터학부", "전자정보공학부\n전자공학전공", "전자정보공학부\nIT융합전공", "글로벌미디어학부", "소프트웨어학부", "AI융합학부", "미디어경영학과", "정보보호학과"]), - .init(college: "경영대학", departments: ["경영학부", "벤처중소기업학과", "회계학과", "금융학부", "벤처경영학과", "혁신경영학과", "복지경영학과", "회계세무학과"]), - .init(college: "경제통상대학", departments: ["경제학과", "글로벌통상학과", "금융경제학과", "국제무역학과"]), - .init(college: "공과대학", departments: ["화학공학과", "신소재공학과", "전기공학부", "기계공학부", "산업ㆍ정보시스템공학과", "건축학부"]), - .init(college: "법과대학", departments: ["법학과", "국제법무학과"]), - .init(college: "사회과학대학", departments: ["사회복지학부", "행정학부", "행정학부", "정보사회학과", "언론홍보학과", "평생교육학과"]), - .init(college: "인문대학", departments: ["기독교학과", "국어국문학과", "영어영문학과", "독어독문학과", "불어불문학과", "중어중문학과", "일어일문학과", "철학과", "철학과", "문예창작전공", "영화예술전공", "스포츠학부"]), - .init(college: "자연과학대학", departments: ["수학과", "물리학과", "화학과", "정보통계∙보험수리학과", "의생명시스템학부"]), - .init(college: "자유전공학부", departments: ["자유전공학부"]), - .init(college: "차세대반도체학과", departments: ["차세대반도체학과"]), - ] - - static var colleges: [String] { list.map(\.college) } - - static func departments(of college: String) -> [String] { - list.first { $0.college == college }?.departments ?? [] - } -} diff --git a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift index d90d96eb..24d5405e 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift @@ -15,11 +15,14 @@ final class SetNickNameView: BaseUIView { // MARK: - Properties private var userNickname: String = "" - public let collegeDropDownView = DropDownView(title: "단과대", items: CollegeDepartmentStore.colleges) + public let collegeDropDownView = DropDownView(title: "단과대", items: []) public let departmentDropDownView = DropDownView(title: "학과", items: []) private var isNicknameChecked = false private var selectedCollege: String? private var selectedDepartment: String? + + public var onSelectCollege: ((String) -> Void)? + public var onSelectDepartment: ((String) -> Void)? // MARK: - UI Components @@ -191,17 +194,20 @@ final class SetNickNameView: BaseUIView { collegeDropDownView.onSelectItem = { [weak self] college in guard let self else { return } selectedCollege = college - let departments = CollegeDepartmentStore.departments(of: college) - departmentDropDownView.updateItems(departments) + + departmentDropDownView.updateItems([]) departmentDropDownView.setTitle("학과") selectedDepartment = nil updateCompleteButtonState() + + onSelectCollege?(college) } departmentDropDownView.onSelectItem = { [weak self] department in guard let self else { return } selectedDepartment = department updateCompleteButtonState() + onSelectDepartment?(department) } } @@ -228,6 +234,14 @@ final class SetNickNameView: BaseUIView { } } } + + public func updateCollegeItems(_ items: [String]) { + collegeDropDownView.updateItems(items) + } + + public func updateDepartmentItems(_ items: [String]) { + departmentDropDownView.updateItems(items) + } } // MARK: - UITextFieldDelegate diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index cf726eb5..46247d82 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -14,24 +14,21 @@ final class SetNickNameViewController: BaseViewController { var currentKeyboardHeight: CGFloat = 0.0 private let nicknameProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) + private let myProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) // MARK: - UI Components private let setNickNameView = SetNickNameView() - private func getDepartments(for college: String) -> [String] { - switch college { - case "인문대": return ["국어국문학과", "영어영문학과", "철학과"] - case "자연대": return ["수학과", "물리학과", "화학과"] - default: return [] - } - } + private var colleges: [LookupItemDTO] = [] + private var departments: [LookupItemDTO] = [] // MARK: - Life Cycles override func viewDidLoad() { super.viewDidLoad() - dismissKeyboard() + bindDropdownCallbacks() + fetchColleges() } override func viewWillAppear(_ animated: Bool) { @@ -64,6 +61,15 @@ final class SetNickNameViewController: BaseViewController { setNickNameView.completeSettingNickNameButton.addTarget(self, action: #selector(tappedCompleteNickNameButton), for: .touchUpInside) setNickNameView.nicknameDoubleCheckButton.addTarget(self, action: #selector(tappedCheckButton), for: .touchUpInside) } + + private func bindDropdownCallbacks() { + setNickNameView.onSelectCollege = { [weak self] collegeName in + guard let self, + let id = self.colleges.first(where: { $0.name == collegeName })?.id + else { return } + self.fetchDepartments(collegeId: id) + } + } @objc func tappedCompleteNickNameButton() { @@ -205,6 +211,44 @@ extension SetNickNameViewController { } } + private func fetchColleges() { + myProvider.request(.colleges) { [weak self] result in + guard let self else { return } + switch result { + case let .success(res): + do { + let decoded = try res.map(CollegesResponseDTO.self) + let list = decoded.result ?? [] + self.colleges = list + self.setNickNameView.updateCollegeItems(list.map(\.name)) + } catch { + print("단과대 파싱 실패: \(error.localizedDescription)") + } + case let .failure(err): + print("단과대 조회 실패: \(err.localizedDescription)") + } + } + } + + private func fetchDepartments(collegeId: Int) { + myProvider.request(.departments(collegeId: collegeId)) { [weak self] result in + guard let self else { return } + switch result { + case let .success(res): + do { + let decoded = try res.map(DepartmentsResponseDTO.self) + let list = decoded.result ?? [] + self.departments = list + self.setNickNameView.updateDepartmentItems(list.map(\.name)) + } catch { + print("학과 파싱 실패: \(error.localizedDescription)") + } + case let .failure(err): + print("학과 조회 실패: \(err.localizedDescription)") + } + } + } + private func showCompletionAlert() { self.showAlertController(title: "완료", message: "정보 수정이 완료되었습니다.", From 51d882ea1b326ef0bb1f4456ac1cecd580634814 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sun, 24 Aug 2025 23:08:02 +0900 Subject: [PATCH 38/52] =?UTF-8?q?[#234]=20=EB=82=B4=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20dto=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EATSSU/App/Sources/Data/Network/DTO/My/MyInfoResponse.swift | 4 ++++ .../Auth/ViewController/LoginViewController.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/EATSSU/App/Sources/Data/Network/DTO/My/MyInfoResponse.swift b/EATSSU/App/Sources/Data/Network/DTO/My/MyInfoResponse.swift index 0bddd01b..c197ece3 100644 --- a/EATSSU/App/Sources/Data/Network/DTO/My/MyInfoResponse.swift +++ b/EATSSU/App/Sources/Data/Network/DTO/My/MyInfoResponse.swift @@ -10,4 +10,8 @@ import Foundation struct MyInfoResponse: Codable { let nickname: String? let provider: String + let departmentId: Int? + let departmentName: String? + let collegeId: Int? + let collegeName: String? } diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index 43f79c64..cd5a13fe 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift @@ -265,6 +265,10 @@ extension LoginViewController { guard let responseData = responseData.result else { return } + // 디버그 모드일 때만 받아온 유저 정보를 출력합니다. +// #if DEBUG + print("현재 로그인 정보: \(responseData)") +// #endif handleNicknameCheck(info: responseData) } catch { print(error.localizedDescription) From 956d2c9ace049b3970a9538613a1a17d9676340e Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:21:37 +0900 Subject: [PATCH 39/52] =?UTF-8?q?[#234]=20=ED=95=99=EA=B3=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20API=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(departmentName=20->=20departmentId)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Network/Router/UserNicknameRouter.swift | 6 +++--- .../ViewController/LoginViewController.swift | 2 +- .../SetNickNameViewController.swift | 16 +++++++++------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift b/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift index 1ee43688..79cb1f39 100644 --- a/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift +++ b/EATSSU/App/Sources/Data/Network/Router/UserNicknameRouter.swift @@ -11,7 +11,7 @@ import Moya enum UserNicknameRouter { case setNickname(nickname: String) case checkNickname(nickname: String) - case setDepartment(department: String) + case setDepartment(departmentId: Int) } extension UserNicknameRouter: TargetType { @@ -49,8 +49,8 @@ extension UserNicknameRouter: TargetType { case let .checkNickname(nickname: nickname): let param: [String: String] = ["nickname": nickname] return .requestParameters(parameters: param, encoding: URLEncoding.queryString) - case let .setDepartment(department: department): - let param: [String: String] = ["departmentName": department] + case let .setDepartment(departmentId: departmentId): + let param: [String: Int] = ["departmentId": departmentId] return .requestParameters(parameters: param, encoding: JSONEncoding.default) } } diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index cd5a13fe..e5c1a11a 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift @@ -122,7 +122,7 @@ final class LoginViewController: BaseViewController { private func storeTokensAndPrintDebugLogs(accessToken: String, refreshToken: String) { RealmService.shared.addToken(accessToken: accessToken, refreshToken: refreshToken) #if DEBUG - print("⭐️⭐️ 토큰 저장 성공 ⭐️⭐️") + print("⭐️⭐️ 토큰 저장 성공 ⭐️⭐️", accessToken) #endif } diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index 46247d82..2deae164 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -74,18 +74,20 @@ final class SetNickNameViewController: BaseViewController { @objc func tappedCompleteNickNameButton() { let nickname = setNickNameView.inputNickNameTextField.text ?? "" - let department = setNickNameView.departmentDropDownView.getSelectedTitle() ?? "" - + guard let departmentName = setNickNameView.departmentDropDownView.getSelectedTitle(), + let departmentId = self.departments.first(where: { $0.name == departmentName })?.id else { + print("학과를 선택해주세요 또는 유효하지 않은 학과입니다.") + return + } setUserNickname(nickname) { [weak self] nickOK in guard nickOK, let self else { return } - self.setUserDepartment(department) { deptOK in + self.setUserDepartment(departmentId: departmentId) { deptOK in guard deptOK else { return } self.showCompletionAlert() } } } - @objc private func tappedCheckButton() { checkNickname(nickname: setNickNameView.inputNickNameTextField.text ?? "") @@ -196,11 +198,11 @@ extension SetNickNameViewController { } } - private func setUserDepartment(_ department: String, completion: @escaping (Bool) -> Void) { - nicknameProvider.request(.setDepartment(department: department)) { result in + private func setUserDepartment(departmentId: Int, completion: @escaping (Bool) -> Void) { + nicknameProvider.request(.setDepartment(departmentId: departmentId)) { result in switch result { case .success: - print("학과 등록 성공: \(department)") + print("학과 등록 성공: ID \(departmentId)") completion(true) case .failure(let error): print("학과 등록 실패: \(error.localizedDescription)") From 44cb3f31deede22523f821ea6ea7f790ffe78482 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:11:30 +0900 Subject: [PATCH 40/52] =?UTF-8?q?[#234]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MyPage/Enum/MyPageLabels.swift | 5 +++- .../MyPage/Model/MyPageLocalData.swift | 5 +++- .../MyPage/View/MyPageView/MyPageView.swift | 26 +++++++------------ .../ViewController/MyPageViewController.swift | 18 +++++-------- .../Sources/Utility/Literal/TextLiteral.swift | 5 +++- 5 files changed, 29 insertions(+), 30 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageLabels.swift b/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageLabels.swift index 7f7f1f85..eda3f837 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageLabels.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/Enum/MyPageLabels.swift @@ -12,7 +12,10 @@ enum MyPageLabels: Int { /// 푸시 알림 설정 case NotificationSetting = 0 - /// 내가 쓴 리뷰 + /// 내 정보 + case MyInfo + + /// 내 리뷰 case MyReview /// 문의하기 diff --git a/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageLocalData.swift b/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageLocalData.swift index b67baed6..3dbd6fa9 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageLocalData.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/Model/MyPageLocalData.swift @@ -18,7 +18,10 @@ extension MyPageLocalData { // "푸시 알림 설정" MyPageLocalData(titleLabel: TextLiteral.MyPage.pushNotificationSetting), - // "내가 쓴 리뷰" + // "내 정보" + MyPageLocalData(titleLabel: TextLiteral.MyPage.MyInfo), + + // "내 리뷰" MyPageLocalData(titleLabel: TextLiteral.MyPage.myReview), // "문의하기" diff --git a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift index 69afdd58..6c8b3ee5 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift @@ -28,14 +28,12 @@ final class MyPageView: BaseUIView { }() // 닉네임이 들어간 닉네임 변경 버튼 - var userNicknameButton: UIButton = { - let button = UIButton() - button.addTitleAttribute( - title: "다시 시도해주세요", - titleColor: .black, - fontName: EATSSUDesignFontFamily.Pretendard.regular.font(size: 16) - ) - return button + var userNicknameLabel: UILabel = { + let label = UILabel() + label.text = "다시 시도해주세요" + label.textColor = .black + label.font = EATSSUDesignFontFamily.Pretendard.semiBold.font(size: 20) + return label }() let myPageTableView: UITableView = { @@ -97,7 +95,7 @@ final class MyPageView: BaseUIView { contentView.addSubviews( userImage, - userNicknameButton, + userNicknameLabel, myPageTableView, appVersionStringLabel, appVersionLabel, @@ -122,14 +120,14 @@ final class MyPageView: BaseUIView { $0.height.width.equalTo(100) } - userNicknameButton.snp.makeConstraints { + userNicknameLabel.snp.makeConstraints { $0.top.equalTo(userImage.snp.bottom).offset(6) $0.centerX.equalTo(userImage) $0.height.equalTo(40) } myPageTableView.snp.makeConstraints { - $0.top.equalTo(userNicknameButton.snp.bottom).offset(16) + $0.top.equalTo(userNicknameLabel.snp.bottom).offset(16) $0.leading.trailing.equalToSuperview() $0.height.equalTo(420) $0.width.equalToSuperview() @@ -171,10 +169,6 @@ final class MyPageView: BaseUIView { } public func setUserInfo(nickname: String) { - userNicknameButton.addTitleAttribute( - title: "\(nickname) >", - titleColor: .black, - fontName: .semiBold(size: 20) - ) + userNicknameLabel.text = nickname } } diff --git a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift index 1e6a8475..8224d1dd 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/ViewController/MyPageViewController.swift @@ -61,9 +61,6 @@ final class MyPageViewController: BaseViewController { } override func setButtonEvent() { - mypageView.userNicknameButton - .addTarget(self, action: #selector(didTappedChangeNicknameButton), for: .touchUpInside) - mypageView.userWithdrawButton .addTarget(self, action: #selector(userWithdrawButtonTapped), for: .touchUpInside) } @@ -76,13 +73,7 @@ final class MyPageViewController: BaseViewController { Analytics.logEvent("MypageViewControllerLoad", parameters: nil) #endif } - - @objc - private func didTappedChangeNicknameButton() { - let setNickNameVC = SetNickNameViewController() - navigationController?.pushViewController(setNickNameVC, animated: true) - } - + @objc private func userWithdrawButtonTapped() { let userWithdrawViewController = UserWithdrawViewController(nickName: nickName) @@ -231,8 +222,13 @@ extension MyPageViewController: UITableViewDelegate { } } } + + // "내 정보" 스크린으로 이동 + case MyPageLabels.MyInfo.rawValue: + let setNickNameVC = SetNickNameViewController() + navigationController?.pushViewController(setNickNameVC, animated: true) - // "내가 쓴 리뷰" 스크린으로 이동 + // "내 리뷰" 스크린으로 이동 case MyPageLabels.MyReview.rawValue: let myReviewViewController = MyReviewViewController(nickname: nickName) navigationController?.pushViewController(myReviewViewController, animated: true) diff --git a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift index 6af79b0f..d725b413 100644 --- a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift +++ b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift @@ -95,12 +95,15 @@ enum TextLiteral { /// "마이페이지 static let myPage: String = "마이페이지" + + /// "내 정보" + static let MyInfo: String = "내 정보" /// "연결된 계정" static let linkedAccount: String = "연결된 계정" /// "내가 쓴 리뷰" - static let myReview: String = "내가 쓴 리뷰" + static let myReview: String = "내 리뷰" /// "로그아웃" static let logout: String = "로그아웃" From b4330acd3926b18524400960e44979e28cc936f4 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:17:39 +0900 Subject: [PATCH 41/52] =?UTF-8?q?[#234]=20=EB=82=B4=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=201=EC=B0=A8=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 --- .../App/Sources/Data/LocalDB/UserInfo.swift | 19 ++ .../Data/LocalDB/UserInfoManager.swift | 33 +++- .../Auth/View/SetNickNameView.swift | 16 -- .../ViewController/LoginViewController.swift | 9 +- .../SetNickNameViewController.swift | 169 ++++++++++++++++-- 5 files changed, 211 insertions(+), 35 deletions(-) diff --git a/EATSSU/App/Sources/Data/LocalDB/UserInfo.swift b/EATSSU/App/Sources/Data/LocalDB/UserInfo.swift index 391e2daf..911ef37f 100644 --- a/EATSSU/App/Sources/Data/LocalDB/UserInfo.swift +++ b/EATSSU/App/Sources/Data/LocalDB/UserInfo.swift @@ -12,6 +12,10 @@ class UserInfo: Object { @Persisted(primaryKey: true) var id: String = UUID().uuidString @Persisted var nickname: String = "" @Persisted private var accountTypeRaw: String? + @Persisted var collegeId: Int? + @Persisted var collegeName: String? + @Persisted var departmentId: Int? + @Persisted var departmentName: String? var accountType: AccountType? { get { @@ -27,10 +31,25 @@ class UserInfo: Object { self.init() self.accountType = accountType } + + func updateUserInfo(nickname: String, collegeId: Int?, collegeName: String?, departmentId: Int?, departmentName: String?) { + self.nickname = nickname + self.collegeId = collegeId + self.collegeName = collegeName + self.departmentId = departmentId + self.departmentName = departmentName + } func updateNickname(_ nickname: String) { self.nickname = nickname } + + func updateDepartment(collegeId: Int?, collegeName: String?, departmentId: Int?, departmentName: String?) { + self.collegeId = collegeId + self.collegeName = collegeName + self.departmentId = departmentId + self.departmentName = departmentName + } enum AccountType: String { case apple = "Apple" diff --git a/EATSSU/App/Sources/Data/LocalDB/UserInfoManager.swift b/EATSSU/App/Sources/Data/LocalDB/UserInfoManager.swift index 20f0349a..bf2c7820 100644 --- a/EATSSU/App/Sources/Data/LocalDB/UserInfoManager.swift +++ b/EATSSU/App/Sources/Data/LocalDB/UserInfoManager.swift @@ -31,17 +31,44 @@ class UserInfoManager { return userInfo } } + + func updateUserInfo(for userInfo: UserInfo, nickname: String, collegeId: Int?, collegeName: String?, departmentId: Int?, departmentName: String?) { + do { + try realm.write { + userInfo.updateUserInfo(nickname: nickname, + collegeId: collegeId, + collegeName: collegeName, + departmentId: departmentId, + departmentName: departmentName) + } + } catch { + print("사용자 정보 업데이트 중 오류 발생: \(error)") + } + } func updateNickname(for userInfo: UserInfo, nickname: String) { do { try realm.write { - userInfo.updateNickname(nickname) + userInfo.updateNickname(nickname) // UserInfo.swift에 있는 함수 호출 } } catch { - print("닉네임 업데이트 중 오류 발생: \(error)") + print("닉네임 정보 업데이트 중 오류 발생: \(error)") } } - + + func updateDepartment(for userInfo: UserInfo, collegeId: Int?, collegeName: String?, departmentId: Int?, departmentName: String?) { + do { + try realm.write { + userInfo.updateDepartment(collegeId: collegeId, + collegeName: collegeName, + departmentId: departmentId, + departmentName: departmentName) + } + } catch { + print("학과 정보 업데이트 중 오류 발생: \(error)") + } + } + func getCurrentUserInfo() -> UserInfo? { realm.objects(UserInfo.self).first } diff --git a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift index 24d5405e..72352292 100644 --- a/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift +++ b/EATSSU/App/Sources/Presentation/Auth/View/SetNickNameView.swift @@ -251,22 +251,6 @@ extension SetNickNameView: UITextFieldDelegate { textField.resignFirstResponder() return true } - - func textFieldDidChangeSelection(_ textField: UITextField) { - guard let inputValue = textField.text?.trimmingCharacters(in: .whitespaces) else { return } - - if inputValue.isEmpty { - textFieldSettingWhenEmpty(textField) - return - } - checkNicknameValidation(textField) - } - - func textFieldShouldClear(_: UITextField) -> Bool { - nicknameDoubleCheckButton.isEnabled = false - completeSettingNickNameButton.isEnabled = false - return true - } } // MARK: - Validation User Information diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift index e5c1a11a..9c6a49f5 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/LoginViewController.swift @@ -108,7 +108,14 @@ final class LoginViewController: BaseViewController { if let nickname = info.nickname { // 사용자의 닉네임을 업데이트하고 홈 화면으로 이동 if let currentUserInfo = UserInfoManager.shared.getCurrentUserInfo() { - UserInfoManager.shared.updateNickname(for: currentUserInfo, nickname: nickname) + UserInfoManager.shared.updateUserInfo( + for: currentUserInfo, + nickname: nickname, + collegeId: info.collegeId, + collegeName: info.collegeName, + departmentId: info.departmentId, + departmentName: info.departmentName + ) } changeIntoHomeViewController() } else { diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index 2deae164..a5e1aade 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -15,7 +15,10 @@ final class SetNickNameViewController: BaseViewController { var currentKeyboardHeight: CGFloat = 0.0 private let nicknameProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) private let myProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) - + private var originalNickname: String? + private var originalDepartmentName: String? + private var isNicknameChecked: Bool = false + // MARK: - UI Components private let setNickNameView = SetNickNameView() @@ -29,6 +32,11 @@ final class SetNickNameViewController: BaseViewController { dismissKeyboard() bindDropdownCallbacks() fetchColleges() + + setNickNameView.inputNickNameTextField.addTarget(self, + action: #selector(nicknameTextFieldDidChange), + for: .editingChanged) + } override func viewWillAppear(_ animated: Bool) { @@ -68,26 +76,137 @@ final class SetNickNameViewController: BaseViewController { let id = self.colleges.first(where: { $0.name == collegeName })?.id else { return } self.fetchDepartments(collegeId: id) + self.updateSaveButtonState() + } + setNickNameView.onSelectDepartment = { [weak self] departmentName in + self?.updateSaveButtonState() + } + } + + private func populateUIWithSavedData() { + guard let userInfo = UserInfoManager.shared.getCurrentUserInfo() else { return } + + self.originalNickname = userInfo.nickname + self.originalDepartmentName = userInfo.departmentName + + setNickNameView.inputNickNameTextField.text = userInfo.nickname + setNickNameView.nicknameDoubleCheckButton.isEnabled = false + setNickNameView.completeSettingNickNameButton.isEnabled = false + + if let collegeName = userInfo.collegeName, + let collegeId = self.colleges.first(where: { $0.name == collegeName })?.id { + setNickNameView.collegeDropDownView.setTitle(collegeName) + + fetchDepartments(collegeId: collegeId) { [weak self] in + if let departmentName = userInfo.departmentName { + self?.setNickNameView.departmentDropDownView.setTitle(departmentName) + } + } } } @objc func tappedCompleteNickNameButton() { - let nickname = setNickNameView.inputNickNameTextField.text ?? "" - guard let departmentName = setNickNameView.departmentDropDownView.getSelectedTitle(), - let departmentId = self.departments.first(where: { $0.name == departmentName })?.id else { - print("학과를 선택해주세요 또는 유효하지 않은 학과입니다.") + let newNickname = setNickNameView.inputNickNameTextField.text ?? "" + let selectedDepartmentName = setNickNameView.departmentDropDownView.getSelectedTitle() + + let hasNicknameChanged = newNickname != self.originalNickname + let hasDepartmentChanged = selectedDepartmentName != self.originalDepartmentName && selectedDepartmentName != nil + + // 변경 사항이 없으면 함수를 종료합니다. (버튼 비활성화 로직으로 인해 호출될 일은 없지만, 안전장치) + guard hasNicknameChanged || hasDepartmentChanged else { + print("변경된 정보가 없습니다.") return } - setUserNickname(nickname) { [weak self] nickOK in - guard nickOK, let self else { return } - self.setUserDepartment(departmentId: departmentId) { deptOK in - guard deptOK else { return } + let dispatchGroup = DispatchGroup() + var isNicknameUpdateSuccess = true + var isDepartmentUpdateSuccess = true + + // 1. 닉네임이 변경되었을 경우에만 닉네임 설정 API 호출 + if hasNicknameChanged { + dispatchGroup.enter() + setUserNickname(newNickname) { success in + isNicknameUpdateSuccess = success + dispatchGroup.leave() + } + } + + // 2. 소속 학과가 변경되었을 경우에만 학과 설정 API 호출 + if hasDepartmentChanged { + guard let departmentName = selectedDepartmentName, + let departmentId = self.departments.first(where: { $0.name == departmentName })?.id else { + print("유효하지 않은 학과 정보입니다.") + return + } + dispatchGroup.enter() + setUserDepartment(departmentId: departmentId) { success in + isDepartmentUpdateSuccess = success + dispatchGroup.leave() + } + } + + // 3. 모든 API 호출이 완료된 후, 결과에 따라 처리 + dispatchGroup.notify(queue: .main) { + if isNicknameUpdateSuccess && isDepartmentUpdateSuccess { self.showCompletionAlert() + } else { + // 각 API 호출 실패 시 이미 로그인 화면으로 전환하는 로직이 있으므로, + // 여기서는 별도 처리가 필요하지 않습니다. + print("정보 업데이트 중 오류가 발생했습니다.") } } } + + @objc + private func nicknameTextFieldDidChange(_ textField: UITextField) { + let newNickname = textField.text ?? "" + let isNicknameChanged = (newNickname != originalNickname) + + // 닉네임이 변경되었다면, 중복 확인 상태를 초기화합니다. + if isNicknameChanged { + self.isNicknameChecked = false + } + + // 닉네임이 비어있는 경우 + if newNickname.isEmpty { + setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.textFieldEmpty.hintMessage + setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.textFieldEmpty.textColor + setNickNameView.nicknameDoubleCheckButton.isEnabled = false + // 글자 수 유효성 검사 (2~8자) + } else if !(2...8).contains(newNickname.count) { + setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.nicknameTextFieldOver.hintMessage + setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.nicknameTextFieldOver.textColor + setNickNameView.nicknameDoubleCheckButton.isEnabled = false + // 닉네임이 변경되었고, 글자 수도 유효한 경우 + } else if isNicknameChanged { + setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.nicknameTextFieldDoubleCheck.hintMessage + setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.nicknameTextFieldDoubleCheck.textColor + setNickNameView.nicknameDoubleCheckButton.isEnabled = true + // 닉네임이 원래대로 돌아온 경우 + } else { + setNickNameView.nicknameValidationMessageLabel.text = "" // 안내 문구 없음 + setNickNameView.nicknameDoubleCheckButton.isEnabled = false + } + + updateSaveButtonState() + } + + private func updateSaveButtonState() { + let nicknameText = setNickNameView.inputNickNameTextField.text ?? "" + let selectedDepartment = setNickNameView.departmentDropDownView.getSelectedTitle() + + // 조건 1: 닉네임 상태가 유효한가? (원래 닉네임이거나, 변경 후 중복 확인을 통과했거나) + let isNicknameStateValid = (nicknameText == originalNickname) || isNicknameChecked + + // 조건 2: 무언가 변경되었는가? (닉네임이 다르거나, 학과가 다르거나) + let hasNicknameChanged = (nicknameText != originalNickname) + let hasDepartmentChanged = (selectedDepartment != originalDepartmentName) && (selectedDepartment != nil) + + // 최종 결정: 닉네임 상태가 유효하고, 무언가 변경되었을 때만 '저장하기' 버튼 활성화 + setNickNameView.completeSettingNickNameButton.isEnabled = isNicknameStateValid && (hasNicknameChanged || hasDepartmentChanged) + } + @objc private func tappedCheckButton() { checkNickname(nickname: setNickNameView.inputNickNameTextField.text ?? "") @@ -148,7 +267,8 @@ final class SetNickNameViewController: BaseViewController { extension SetNickNameViewController { private func setUserNickname(_ nickname: String, completion: @escaping (Bool) -> Void) { - nicknameProvider.request(.setNickname(nickname: nickname)) { result in + nicknameProvider.request(.setNickname(nickname: nickname)) { [weak self] result in + guard let self = self else { return } switch result { case .success: if let user = UserInfoManager.shared.getCurrentUserInfo() { @@ -176,12 +296,16 @@ extension SetNickNameViewController { self.setNickNameView.setNicknameChecked(true) self.setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.nicknameTextFieldValid.hintMessage self.setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.nicknameTextFieldValid.textColor + self.setNickNameView.completeSettingNickNameButton.isEnabled = true + self.isNicknameChecked = true } else { self.view.showToast(message: "이미 사용 중인 닉네임이에요") self.setNickNameView.completeSettingNickNameButton.isEnabled = data self.setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.nicknameTextFieldDuplicated.hintMessage self.setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.nicknameTextFieldDuplicated.textColor + self.isNicknameChecked = false } + self.updateSaveButtonState() } catch let err { print(err.localizedDescription) @@ -203,6 +327,17 @@ extension SetNickNameViewController { switch result { case .success: print("학과 등록 성공: ID \(departmentId)") + if let user = UserInfoManager.shared.getCurrentUserInfo() { + let collegeName = self.setNickNameView.collegeDropDownView.getSelectedTitle() + let departmentName = self.setNickNameView.departmentDropDownView.getSelectedTitle() + let collegeId = self.colleges.first(where: { $0.name == collegeName })?.id + + UserInfoManager.shared.updateDepartment(for: user, + collegeId: collegeId, + collegeName: collegeName, + departmentId: departmentId, + departmentName: departmentName) + } completion(true) case .failure(let error): print("학과 등록 실패: \(error.localizedDescription)") @@ -219,10 +354,11 @@ extension SetNickNameViewController { switch result { case let .success(res): do { - let decoded = try res.map(CollegesResponseDTO.self) - let list = decoded.result ?? [] - self.colleges = list - self.setNickNameView.updateCollegeItems(list.map(\.name)) + let decoded = try res.map(CollegesResponseDTO.self) + let list = decoded.result ?? [] + self.colleges = list + self.setNickNameView.updateCollegeItems(list.map(\.name)) + self.populateUIWithSavedData() } catch { print("단과대 파싱 실패: \(error.localizedDescription)") } @@ -232,7 +368,7 @@ extension SetNickNameViewController { } } - private func fetchDepartments(collegeId: Int) { + private func fetchDepartments(collegeId: Int, completion: (() -> Void)? = nil) { myProvider.request(.departments(collegeId: collegeId)) { [weak self] result in guard let self else { return } switch result { @@ -242,11 +378,14 @@ extension SetNickNameViewController { let list = decoded.result ?? [] self.departments = list self.setNickNameView.updateDepartmentItems(list.map(\.name)) + completion?() } catch { print("학과 파싱 실패: \(error.localizedDescription)") + completion?() } case let .failure(err): print("학과 조회 실패: \(err.localizedDescription)") + completion?() } } } From 83481bc6533e2693c6414246c23cb2e6f9a5c3de Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:52:56 +0900 Subject: [PATCH 42/52] =?UTF-8?q?[#234]=20SetNickNameViewController=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81,=20Ap?= =?UTF-8?q?pDelegate=EC=97=90=20Realm=20=EB=A7=88=EC=9D=B4=ED=81=AC?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SetNickNameViewController.swift | 211 +++++++----------- .../Utility/Application/AppDelegate.swift | 15 ++ 2 files changed, 90 insertions(+), 136 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index a5e1aade..c0cf3847 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -11,32 +11,26 @@ import Moya final class SetNickNameViewController: BaseViewController { // MARK: - Properties - - var currentKeyboardHeight: CGFloat = 0.0 + private let nicknameProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) private let myProvider = MoyaProvider(session: Session(interceptor: AuthInterceptor.shared)) private var originalNickname: String? private var originalDepartmentName: String? private var isNicknameChecked: Bool = false - + private var colleges: [LookupItemDTO] = [] + private var departments: [LookupItemDTO] = [] + // MARK: - UI Components private let setNickNameView = SetNickNameView() - private var colleges: [LookupItemDTO] = [] - private var departments: [LookupItemDTO] = [] // MARK: - Life Cycles - + override func viewDidLoad() { super.viewDidLoad() dismissKeyboard() - bindDropdownCallbacks() + bindUI() fetchColleges() - - setNickNameView.inputNickNameTextField.addTarget(self, - action: #selector(nicknameTextFieldDidChange), - for: .editingChanged) - } override func viewWillAppear(_ animated: Bool) { @@ -44,11 +38,11 @@ final class SetNickNameViewController: BaseViewController { setNickNameView.setAccountInfo() } - override func viewWillDisappear(_: Bool) { - removeKeyboardNotifications() + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) } - // MARK: - Functions + // MARK: - UI Configuration override func configureUI() { view.addSubviews(setNickNameView) @@ -59,18 +53,18 @@ final class SetNickNameViewController: BaseViewController { $0.edges.equalToSuperview() } } - + override func setCustomNavigationBar() { super.setCustomNavigationBar() navigationItem.title = TextLiteral.setNickName } - - override func setButtonEvent() { + + private func bindUI() { setNickNameView.completeSettingNickNameButton.addTarget(self, action: #selector(tappedCompleteNickNameButton), for: .touchUpInside) setNickNameView.nicknameDoubleCheckButton.addTarget(self, action: #selector(tappedCheckButton), for: .touchUpInside) - } - - private func bindDropdownCallbacks() { + setNickNameView.inputNickNameTextField.addTarget(self, + action: #selector(nicknameTextFieldDidChange), + for: .editingChanged) setNickNameView.onSelectCollege = { [weak self] collegeName in guard let self, let id = self.colleges.first(where: { $0.name == collegeName })?.id @@ -78,42 +72,20 @@ final class SetNickNameViewController: BaseViewController { self.fetchDepartments(collegeId: id) self.updateSaveButtonState() } - setNickNameView.onSelectDepartment = { [weak self] departmentName in + setNickNameView.onSelectDepartment = { [weak self] _ in self?.updateSaveButtonState() } } - - private func populateUIWithSavedData() { - guard let userInfo = UserInfoManager.shared.getCurrentUserInfo() else { return } - - self.originalNickname = userInfo.nickname - self.originalDepartmentName = userInfo.departmentName - - setNickNameView.inputNickNameTextField.text = userInfo.nickname - setNickNameView.nicknameDoubleCheckButton.isEnabled = false - setNickNameView.completeSettingNickNameButton.isEnabled = false - - if let collegeName = userInfo.collegeName, - let collegeId = self.colleges.first(where: { $0.name == collegeName })?.id { - setNickNameView.collegeDropDownView.setTitle(collegeName) - - fetchDepartments(collegeId: collegeId) { [weak self] in - if let departmentName = userInfo.departmentName { - self?.setNickNameView.departmentDropDownView.setTitle(departmentName) - } - } - } - } - @objc - func tappedCompleteNickNameButton() { + // MARK: - @objc Methods + + @objc private func tappedCompleteNickNameButton() { let newNickname = setNickNameView.inputNickNameTextField.text ?? "" let selectedDepartmentName = setNickNameView.departmentDropDownView.getSelectedTitle() let hasNicknameChanged = newNickname != self.originalNickname let hasDepartmentChanged = selectedDepartmentName != self.originalDepartmentName && selectedDepartmentName != nil - // 변경 사항이 없으면 함수를 종료합니다. (버튼 비활성화 로직으로 인해 호출될 일은 없지만, 안전장치) guard hasNicknameChanged || hasDepartmentChanged else { print("변경된 정보가 없습니다.") return @@ -123,7 +95,6 @@ final class SetNickNameViewController: BaseViewController { var isNicknameUpdateSuccess = true var isDepartmentUpdateSuccess = true - // 1. 닉네임이 변경되었을 경우에만 닉네임 설정 API 호출 if hasNicknameChanged { dispatchGroup.enter() setUserNickname(newNickname) { success in @@ -132,66 +103,89 @@ final class SetNickNameViewController: BaseViewController { } } - // 2. 소속 학과가 변경되었을 경우에만 학과 설정 API 호출 if hasDepartmentChanged { guard let departmentName = selectedDepartmentName, let departmentId = self.departments.first(where: { $0.name == departmentName })?.id else { print("유효하지 않은 학과 정보입니다.") return } + let collegeName = setNickNameView.collegeDropDownView.getSelectedTitle() + let collegeId = self.colleges.first(where: { $0.name == collegeName })?.id + dispatchGroup.enter() - setUserDepartment(departmentId: departmentId) { success in + setUserDepartment(departmentInfo: (id: departmentId, name: departmentName), + collegeInfo: (id: collegeId, name: collegeName)) { success in isDepartmentUpdateSuccess = success dispatchGroup.leave() } } - // 3. 모든 API 호출이 완료된 후, 결과에 따라 처리 dispatchGroup.notify(queue: .main) { if isNicknameUpdateSuccess && isDepartmentUpdateSuccess { self.showCompletionAlert() } else { - // 각 API 호출 실패 시 이미 로그인 화면으로 전환하는 로직이 있으므로, - // 여기서는 별도 처리가 필요하지 않습니다. print("정보 업데이트 중 오류가 발생했습니다.") } } } - @objc - private func nicknameTextFieldDidChange(_ textField: UITextField) { + @objc private func nicknameTextFieldDidChange(_ textField: UITextField) { let newNickname = textField.text ?? "" let isNicknameChanged = (newNickname != originalNickname) - // 닉네임이 변경되었다면, 중복 확인 상태를 초기화합니다. if isNicknameChanged { self.isNicknameChecked = false } - // 닉네임이 비어있는 경우 if newNickname.isEmpty { setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.textFieldEmpty.hintMessage setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.textFieldEmpty.textColor setNickNameView.nicknameDoubleCheckButton.isEnabled = false - // 글자 수 유효성 검사 (2~8자) } else if !(2...8).contains(newNickname.count) { setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.nicknameTextFieldOver.hintMessage setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.nicknameTextFieldOver.textColor setNickNameView.nicknameDoubleCheckButton.isEnabled = false - // 닉네임이 변경되었고, 글자 수도 유효한 경우 } else if isNicknameChanged { setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.nicknameTextFieldDoubleCheck.hintMessage setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.nicknameTextFieldDoubleCheck.textColor setNickNameView.nicknameDoubleCheckButton.isEnabled = true - // 닉네임이 원래대로 돌아온 경우 } else { - setNickNameView.nicknameValidationMessageLabel.text = "" // 안내 문구 없음 + setNickNameView.nicknameValidationMessageLabel.text = "" setNickNameView.nicknameDoubleCheckButton.isEnabled = false } updateSaveButtonState() } + @objc + private func tappedCheckButton() { + checkNickname(nickname: setNickNameView.inputNickNameTextField.text ?? "") + } + + // MARK: - Private Methods + + private func populateUIWithSavedData() { + guard let userInfo = UserInfoManager.shared.getCurrentUserInfo() else { return } + + self.originalNickname = userInfo.nickname + self.originalDepartmentName = userInfo.departmentName + + setNickNameView.inputNickNameTextField.text = userInfo.nickname + setNickNameView.nicknameDoubleCheckButton.isEnabled = false + + if let collegeName = userInfo.collegeName, + let collegeId = self.colleges.first(where: { $0.name == collegeName })?.id { + setNickNameView.collegeDropDownView.setTitle(collegeName) + + fetchDepartments(collegeId: collegeId) { [weak self] in + if let departmentName = userInfo.departmentName { + self?.setNickNameView.departmentDropDownView.setTitle(departmentName) + self?.updateSaveButtonState() // 학과 정보까지 로드 후 버튼 상태 최종 업데이트 + } + } + } + } + private func updateSaveButtonState() { let nicknameText = setNickNameView.inputNickNameTextField.text ?? "" let selectedDepartment = setNickNameView.departmentDropDownView.getSelectedTitle() @@ -201,56 +195,11 @@ final class SetNickNameViewController: BaseViewController { // 조건 2: 무언가 변경되었는가? (닉네임이 다르거나, 학과가 다르거나) let hasNicknameChanged = (nicknameText != originalNickname) - let hasDepartmentChanged = (selectedDepartment != originalDepartmentName) && (selectedDepartment != nil) + let hasDepartmentChanged = (selectedDepartment != originalDepartmentName) && (selectedDepartment != "학과") && (selectedDepartment != nil) - // 최종 결정: 닉네임 상태가 유효하고, 무언가 변경되었을 때만 '저장하기' 버튼 활성화 setNickNameView.completeSettingNickNameButton.isEnabled = isNicknameStateValid && (hasNicknameChanged || hasDepartmentChanged) } - @objc - private func tappedCheckButton() { - checkNickname(nickname: setNickNameView.inputNickNameTextField.text ?? "") - } - - // MARK: - keyboard 감지 - - func addKeyboardNotifications() { - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillShow(_:)), - name: UIResponder.keyboardWillShowNotification, - object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), - name: UIResponder.keyboardWillHideNotification, - object: nil) - } - - func removeKeyboardNotifications() { - NotificationCenter.default.removeObserver(self, - name: UIResponder.keyboardWillShowNotification, - object: nil) - NotificationCenter.default.removeObserver(self, - name: UIResponder.keyboardWillHideNotification, - object: nil) - } - - @objc - func keyboardWillShow(_ notification: Notification) { - if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { - let updateKeyboardHeight = keyboardSize.height - let difference = updateKeyboardHeight - currentKeyboardHeight - - setNickNameView.completeSettingNickNameButton.frame.origin.y -= difference - currentKeyboardHeight = updateKeyboardHeight - } - } - - @objc func keyboardWillHide(_ notification: Notification) { - if ((notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue) != nil { - setNickNameView.completeSettingNickNameButton.frame.origin.y += currentKeyboardHeight - currentKeyboardHeight = 0.0 - } - } - private func navigateToLogin() { let loginVC = LoginViewController() loginVC.toastMessage = "세션이 만료되었습니다. 다시 로그인해주세요." @@ -284,29 +233,23 @@ extension SetNickNameViewController { } private func checkNickname(nickname: String) { - nicknameProvider.request(.checkNickname(nickname: nickname)) { response in + nicknameProvider.request(.checkNickname(nickname: nickname)) { [weak self] response in + guard let self = self else { return } switch response { - case let .success(moyaResponse): + case .success(let moyaResponse): do { let responseData = try moyaResponse.map(BaseResponse.self) - guard let data = responseData.result else { return } + let isNicknameAvailable = responseData.result ?? false + let resultType: NicknameTextFieldResultType = isNicknameAvailable ? .nicknameTextFieldValid : .nicknameTextFieldDuplicated + let toastMessage = isNicknameAvailable ? "사용 가능한 닉네임이에요" : "이미 사용 중인 닉네임이에요" - if data { - self.view.showToast(message: "사용 가능한 닉네임이에요") - self.setNickNameView.setNicknameChecked(true) - self.setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.nicknameTextFieldValid.hintMessage - self.setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.nicknameTextFieldValid.textColor - self.setNickNameView.completeSettingNickNameButton.isEnabled = true - self.isNicknameChecked = true - } else { - self.view.showToast(message: "이미 사용 중인 닉네임이에요") - self.setNickNameView.completeSettingNickNameButton.isEnabled = data - self.setNickNameView.nicknameValidationMessageLabel.text = NicknameTextFieldResultType.nicknameTextFieldDuplicated.hintMessage - self.setNickNameView.nicknameValidationMessageLabel.textColor = NicknameTextFieldResultType.nicknameTextFieldDuplicated.textColor - self.isNicknameChecked = false - } + self.isNicknameChecked = isNicknameAvailable + self.view.showToast(message: toastMessage) + self.setNickNameView.nicknameValidationMessageLabel.text = resultType.hintMessage + self.setNickNameView.nicknameValidationMessageLabel.textColor = resultType.textColor + self.setNickNameView.setNicknameChecked(isNicknameAvailable) self.updateSaveButtonState() - + } catch let err { print(err.localizedDescription) @@ -322,21 +265,17 @@ extension SetNickNameViewController { } } - private func setUserDepartment(departmentId: Int, completion: @escaping (Bool) -> Void) { - nicknameProvider.request(.setDepartment(departmentId: departmentId)) { result in + private func setUserDepartment(departmentInfo: (id: Int, name: String?), collegeInfo: (id: Int?, name: String?), completion: @escaping (Bool) -> Void) { + nicknameProvider.request(.setDepartment(departmentId: departmentInfo.id)) { result in switch result { case .success: - print("학과 등록 성공: ID \(departmentId)") + print("학과 등록 성공: ID \(departmentInfo.id)") if let user = UserInfoManager.shared.getCurrentUserInfo() { - let collegeName = self.setNickNameView.collegeDropDownView.getSelectedTitle() - let departmentName = self.setNickNameView.departmentDropDownView.getSelectedTitle() - let collegeId = self.colleges.first(where: { $0.name == collegeName })?.id - UserInfoManager.shared.updateDepartment(for: user, - collegeId: collegeId, - collegeName: collegeName, - departmentId: departmentId, - departmentName: departmentName) + collegeId: collegeInfo.id, + collegeName: collegeInfo.name, + departmentId: departmentInfo.id, + departmentName: departmentInfo.name) } completion(true) case .failure(let error): diff --git a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift index dae341f4..00a2f966 100644 --- a/EATSSU/App/Sources/Utility/Application/AppDelegate.swift +++ b/EATSSU/App/Sources/Utility/Application/AppDelegate.swift @@ -11,12 +11,14 @@ import UIKit import Firebase import KakaoSDKCommon import NMapsMap +import RealmSwift @main class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { // MARK: - UIApplicationDelegate Methods func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + configureRealm() setupNotificationPermissions() startNetworkMonitoring() configureFirebase() @@ -58,6 +60,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Private Methods + private func configureRealm() { + let config = Realm.Configuration( + // 데이터베이스의 버전을 설정 - 구조를 변경할 때마다 이 숫자를 1씩 증가 + schemaVersion: 1, + migrationBlock: { migration, oldSchemaVersion in + if oldSchemaVersion < 1 { + // UserInfo에 새 속성들이 추가된 경우, Realm이 자동으로 처리 + } + } + ) + Realm.Configuration.defaultConfiguration = config + } + /// 푸시 알림 권한을 요청하고 설정을 처리합니다. private func setupNotificationPermissions() { NotificationManager.shared.requestNotificationPermission { granted in From c2e4e20343927efff3e43a5d313b3eea78f9a0aa Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:03:25 +0900 Subject: [PATCH 43/52] =?UTF-8?q?[#234]=20myPageTableView=EC=9D=98=20heigh?= =?UTF-8?q?t=EA=B0=80=20=EC=9C=A0=EB=8F=99=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=ED=95=98=EA=B2=8C=EB=81=94=20=EB=A6=AC?= =?UTF-8?q?=ED=8E=99=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/MyPage/View/MyPageView/MyPageView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift index 6c8b3ee5..25d38adf 100644 --- a/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift +++ b/EATSSU/App/Sources/Presentation/MyPage/View/MyPageView/MyPageView.swift @@ -129,7 +129,9 @@ final class MyPageView: BaseUIView { myPageTableView.snp.makeConstraints { $0.top.equalTo(userNicknameLabel.snp.bottom).offset(16) $0.leading.trailing.equalToSuperview() - $0.height.equalTo(420) + let cellHeight = 60 + let totalHeight = MyPageLocalData.myPageTableLabelList.count * cellHeight + $0.height.equalTo(totalHeight) $0.width.equalToSuperview() } From a8d19e3f6a2311dbe863d2df1f6a33da5c89778e Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:31:32 +0900 Subject: [PATCH 44/52] =?UTF-8?q?[#234=20=ED=95=98=EB=8B=A8=20=ED=83=AD?= =?UTF-8?q?=EB=B0=94=20=EB=86=92=EC=9D=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/TabBar/CustomTabBarContainerController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index afaba139..98cdf532 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -49,7 +49,7 @@ final class CustomTabBarContainerController: BaseViewController { tabBarView.snp.makeConstraints { $0.leading.trailing.equalToSuperview() $0.bottom.equalTo(view.snp.bottom) - $0.height.equalTo(74) + $0.height.equalTo(80) } } From 1fba51bbef78baac5a35af247c42c7c66e902418 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:52:08 +0900 Subject: [PATCH 45/52] =?UTF-8?q?[#234]=20=EC=A0=9C=ED=9C=B4=20=EB=B0=94?= =?UTF-8?q?=ED=85=80=20=EC=8B=9C=ED=8A=B8=20iOS=2016.0=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=9D=BC=EB=95=8C,=20Height=EA=B0=80=20=EC=9C=A0?= =?UTF-8?q?=EB=8F=99=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=B0=94=EB=80=8C?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainMapViewController.swift | 21 +++++++++++++++--- ...PartnershipDetailSheetViewController.swift | 13 +++++++---- .../SignWithApple.imageset/Contents.json | 2 +- .../SignWithApple.imageset/SignWithApple.pdf | Bin 0 -> 19077 bytes .../SignWithApple.imageset/SignWithApple.svg | 11 --------- .../SignWithKakao.imageset/Contents.json | 2 +- .../SignWithKakao.imageset/SignWithKakao.pdf | Bin 0 -> 11748 bytes .../SignWithKakao.imageset/SignWithKakao.png | Bin 858 -> 0 bytes .../profile.imageset/Contents.json | 13 +++++++++-- .../profile.imageset/profile.png | Bin 0 -> 5113 bytes .../profile.imageset/profile.svg | 7 ------ 11 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/SignWithApple.pdf delete mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/SignWithApple.svg create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/SignWithKakao.pdf delete mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/SignWithKakao.png create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/profile.imageset/profile.png delete mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/profile.imageset/profile.svg diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift index bb203a33..06ddfc51 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift @@ -152,15 +152,30 @@ final class MainMapViewController: BaseViewController, CLLocationManagerDelegate marker.height = CGFloat(UInt32(markerImage.size.height)) marker.touchHandler = { [weak self] _ in - let vc = PartnershipDetailSheetViewController( + let sheetVC = PartnershipDetailSheetViewController( storeName: partnership.storeName, restaurantType: partnership.restaurantType, partnershipInfos: partnership.partnershipInfos ) - self?.present(vc, animated: true) + + if let sheet = sheetVC.sheetPresentationController { + if #available(iOS 16.0, *) { + sheetVC.loadViewIfNeeded() + let contentHeight = sheetVC.calculatePreferredHeight() + let customDetent = UISheetPresentationController.Detent.custom { _ in + return contentHeight + } + sheet.detents = [customDetent, .large()] + } else { + sheet.detents = [.medium(), .large()] + } + sheet.prefersGrabberVisible = true + } + + self?.present(sheetVC, animated: true) + return true } - marker.mapView = root.mapView.mapView markers.append(marker) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 5c6bfe31..7b007d2a 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -36,10 +36,6 @@ final class PartnershipDetailSheetViewController: BaseViewController { super.init(nibName: nil, bundle: nil) modalPresentationStyle = .pageSheet - if let sheet = sheetPresentationController { - sheet.detents = [.medium(), .large()] - sheet.prefersGrabberVisible = true - } } required init?(coder: NSCoder) { @@ -130,6 +126,15 @@ final class PartnershipDetailSheetViewController: BaseViewController { // MARK: - UI Helpers + /// 제휴 content 갯수에 따라 유동적으로 Height 측정 + func calculatePreferredHeight() -> CGFloat { + view.layoutIfNeeded() + let contentHeight = infoListStackView.frame.maxY + let bottomPadding: CGFloat = view.safeAreaInsets.bottom + 20 + + return contentHeight + bottomPadding + } + /// 제휴 정보 카드 뷰 생성 private func makeInfoCard(info: PartnershipInfoDTO, isLast: Bool) -> UIView { let labelText = info.collegeName ?? info.departmentName ?? "학과 정보 없음" diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/Contents.json index 31cef0c5..1703d7c1 100644 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/Contents.json +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "SignWithApple.svg", + "filename" : "SignWithApple.pdf", "idiom" : "universal" } ], diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/SignWithApple.pdf b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/SignWithApple.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d78267a4784f87425deaabf5c942485ea0c34381 GIT binary patch literal 19077 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 - - - - - - - - - - diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/Contents.json index 0c717458..a5fc642b 100644 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/Contents.json +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "SignWithKakao.png", + "filename" : "SignWithKakao.pdf", "idiom" : "universal" } ], diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/SignWithKakao.pdf b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/SignWithKakao.pdf new file mode 100644 index 0000000000000000000000000000000000000000..468b0d3bf9e3cc7fce03abd6e41b141a2a1c8b92 GIT binary patch literal 11748 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 literal 0 HcmV?d00001 diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/SignWithKakao.png b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/SignWithKakao.png deleted file mode 100644 index 86adc64f5f666937e8936db80885ae0bf3c56976..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 858 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 b2<`AcqB;ogYN;=z00000NkvXXu0mjfJ3&JA literal 0 HcmV?d00001 diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/profile.imageset/profile.svg b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/profile.imageset/profile.svg deleted file mode 100644 index c6cb5993..00000000 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/profile.imageset/profile.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - From 2ae22f335c5866961e11bd6ed8af03e5340f1bd8 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:03:54 +0900 Subject: [PATCH 46/52] =?UTF-8?q?[#234]=20=ED=95=98=EB=8B=A8=20=ED=83=AD?= =?UTF-8?q?=EB=B0=94=20=EC=B6=94=EA=B0=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/TabBar/CustomTabBarView.swift | 1 + .../Contents.json | 2 +- .../Property 1=apple.pdf} | Bin .../Contents.json | 2 +- .../Property 1=kakao.pdf} | Bin 5 files changed, 3 insertions(+), 2 deletions(-) rename EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/{SignWithApple.imageset => Property 1=apple.imageset}/Contents.json (74%) rename EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/{SignWithApple.imageset/SignWithApple.pdf => Property 1=apple.imageset/Property 1=apple.pdf} (100%) rename EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/{SignWithKakao.imageset => Property 1=kakao.imageset}/Contents.json (74%) rename EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/{SignWithKakao.imageset/SignWithKakao.pdf => Property 1=kakao.imageset/Property 1=kakao.pdf} (100%) diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift index 563d57e6..66da1539 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarView.swift @@ -29,6 +29,7 @@ final class CustomTabBarView: BaseUIView { config.imagePlacement = .top config.imagePadding = 4 config.baseForegroundColor = .gray + config.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0) let button = UIButton(configuration: config, primaryAction: nil) button.tag = index diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=apple.imageset/Contents.json similarity index 74% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/Contents.json rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=apple.imageset/Contents.json index 1703d7c1..3952aa5c 100644 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/Contents.json +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=apple.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "SignWithApple.pdf", + "filename" : "Property 1=apple.pdf", "idiom" : "universal" } ], diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/SignWithApple.pdf b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=apple.imageset/Property 1=apple.pdf similarity index 100% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithApple.imageset/SignWithApple.pdf rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=apple.imageset/Property 1=apple.pdf diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=kakao.imageset/Contents.json similarity index 74% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/Contents.json rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=kakao.imageset/Contents.json index a5fc642b..152b81b4 100644 --- a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/Contents.json +++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=kakao.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "SignWithKakao.pdf", + "filename" : "Property 1=kakao.pdf", "idiom" : "universal" } ], diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/SignWithKakao.pdf b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=kakao.imageset/Property 1=kakao.pdf similarity index 100% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/SignWithKakao.imageset/SignWithKakao.pdf rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=kakao.imageset/Property 1=kakao.pdf From cd54e60bd2e1ddb5907a4214169be9d36712ccfe Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:48:45 +0900 Subject: [PATCH 47/52] =?UTF-8?q?[#234]=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Contents.json | 0 .../Property 1=apple.pdf | Bin .../Contents.json | 0 .../Property 1=kakao.pdf | Bin 4 files changed, 0 insertions(+), 0 deletions(-) rename EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/{Property 1=apple.imageset => signWithApple.imageset}/Contents.json (100%) rename EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/{Property 1=apple.imageset => signWithApple.imageset}/Property 1=apple.pdf (100%) rename EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/{Property 1=kakao.imageset => signWithKakao.imageset}/Contents.json (100%) rename EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/{Property 1=kakao.imageset => signWithKakao.imageset}/Property 1=kakao.pdf (100%) diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=apple.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/signWithApple.imageset/Contents.json similarity index 100% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=apple.imageset/Contents.json rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/signWithApple.imageset/Contents.json diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=apple.imageset/Property 1=apple.pdf b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/signWithApple.imageset/Property 1=apple.pdf similarity index 100% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=apple.imageset/Property 1=apple.pdf rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/signWithApple.imageset/Property 1=apple.pdf diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=kakao.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/signWithKakao.imageset/Contents.json similarity index 100% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=kakao.imageset/Contents.json rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/signWithKakao.imageset/Contents.json diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=kakao.imageset/Property 1=kakao.pdf b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/signWithKakao.imageset/Property 1=kakao.pdf similarity index 100% rename from EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/Property 1=kakao.imageset/Property 1=kakao.pdf rename to EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/signWithKakao.imageset/Property 1=kakao.pdf From 61dba7b015fdccb8a059bca4476e0246da809e48 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:28:33 +0900 Subject: [PATCH 48/52] =?UTF-8?q?[#234]=20profile=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=A6=AC=EB=B7=B0=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=81=AC=EA=B8=B0=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Review/View/SeeReview/ReviewTableCell.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift index d8a1a2cf..b01cf841 100644 --- a/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift +++ b/EATSSU/App/Sources/Presentation/Review/View/SeeReview/ReviewTableCell.swift @@ -192,6 +192,10 @@ final class ReviewTableCell: UITableViewCell { } func setLayout() { + userProfileImageView.snp.makeConstraints { make in + make.width.height.equalTo(30) + } + profileStackView.snp.makeConstraints { make in make.top.equalToSuperview().offset(5) make.leading.equalToSuperview().offset(16) From 96c3b1bd90648efc96448909b235a6f117e536dd Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:50:03 +0900 Subject: [PATCH 49/52] =?UTF-8?q?[#234]=20=EB=91=98=EB=9F=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EC=9C=A0=EB=8F=84=20alert=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TabBar/CustomTabBarContainerController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift index 98cdf532..197956a2 100644 --- a/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift +++ b/EATSSU/App/Sources/Presentation/TabBar/CustomTabBarContainerController.swift @@ -27,8 +27,9 @@ final class CustomTabBarContainerController: BaseViewController { tabBarView.buttonTapped = { [weak self] index in guard let self = self else { return } - // 마이페이지는 로그인 필요 - if index == 2, RealmService.shared.isAccessTokenPresent() == false { + // 마이페이지와 지도는 로그인 필요 + // TODO: 지도는 서버팀과 함께 나중에 둘러보기 상태에서보 "전체" 카테고리는 볼 수 있게 수정 + if (index == 1 || index == 2), RealmService.shared.isAccessTokenPresent() == false { self.presentLoginAlert() return } From 3409f0e82abd69cd5491e1cd850dcea8d270705b Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:10:40 +0900 Subject: [PATCH 50/52] =?UTF-8?q?[#234]=20=ED=95=99=EA=B3=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=EB=A7=8C=20=EC=88=98=EC=A0=95=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=A0=20=EA=B2=BD=EC=9A=B0=20=EB=9C=A8?= =?UTF-8?q?=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SetNickNameViewController.swift | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift index c0cf3847..405dda18 100644 --- a/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift +++ b/EATSSU/App/Sources/Presentation/Auth/ViewController/SetNickNameViewController.swift @@ -81,13 +81,13 @@ final class SetNickNameViewController: BaseViewController { @objc private func tappedCompleteNickNameButton() { let newNickname = setNickNameView.inputNickNameTextField.text ?? "" - let selectedDepartmentName = setNickNameView.departmentDropDownView.getSelectedTitle() let hasNicknameChanged = newNickname != self.originalNickname - let hasDepartmentChanged = selectedDepartmentName != self.originalDepartmentName && selectedDepartmentName != nil + let departmentChanged = isDepartmentChanged() - guard hasNicknameChanged || hasDepartmentChanged else { + guard hasNicknameChanged || departmentChanged else { print("변경된 정보가 없습니다.") + view.showToast(message: "변경된 정보가 없습니다.") return } @@ -103,8 +103,8 @@ final class SetNickNameViewController: BaseViewController { } } - if hasDepartmentChanged { - guard let departmentName = selectedDepartmentName, + if departmentChanged { + guard let departmentName = setNickNameView.departmentDropDownView.getSelectedTitle(), let departmentId = self.departments.first(where: { $0.name == departmentName })?.id else { print("유효하지 않은 학과 정보입니다.") return @@ -124,7 +124,8 @@ final class SetNickNameViewController: BaseViewController { if isNicknameUpdateSuccess && isDepartmentUpdateSuccess { self.showCompletionAlert() } else { - print("정보 업데이트 중 오류가 발생했습니다.") + // 실패 시 사용자에게 알림 + self.showAlertController(title: "오류", message: "정보 업데이트 중 오류가 발생했습니다.", style: .cancel) } } } @@ -164,6 +165,16 @@ final class SetNickNameViewController: BaseViewController { // MARK: - Private Methods + /// 학과 변경 여부를 명확하게 판단하는 헬퍼 메서드 + private func isDepartmentChanged() -> Bool { + guard let selectedDepartment = setNickNameView.departmentDropDownView.getSelectedTitle(), + selectedDepartment != "학과" + else { + return false + } + return selectedDepartment != originalDepartmentName + } + private func populateUIWithSavedData() { guard let userInfo = UserInfoManager.shared.getCurrentUserInfo() else { return } @@ -188,16 +199,15 @@ final class SetNickNameViewController: BaseViewController { private func updateSaveButtonState() { let nicknameText = setNickNameView.inputNickNameTextField.text ?? "" - let selectedDepartment = setNickNameView.departmentDropDownView.getSelectedTitle() // 조건 1: 닉네임 상태가 유효한가? (원래 닉네임이거나, 변경 후 중복 확인을 통과했거나) let isNicknameStateValid = (nicknameText == originalNickname) || isNicknameChecked // 조건 2: 무언가 변경되었는가? (닉네임이 다르거나, 학과가 다르거나) let hasNicknameChanged = (nicknameText != originalNickname) - let hasDepartmentChanged = (selectedDepartment != originalDepartmentName) && (selectedDepartment != "학과") && (selectedDepartment != nil) + let departmentChanged = isDepartmentChanged() - setNickNameView.completeSettingNickNameButton.isEnabled = isNicknameStateValid && (hasNicknameChanged || hasDepartmentChanged) + setNickNameView.completeSettingNickNameButton.isEnabled = isNicknameStateValid && (hasNicknameChanged || departmentChanged) } private func navigateToLogin() { @@ -213,7 +223,6 @@ final class SetNickNameViewController: BaseViewController { } // MARK: - Network - extension SetNickNameViewController { private func setUserNickname(_ nickname: String, completion: @escaping (Bool) -> Void) { nicknameProvider.request(.setNickname(nickname: nickname)) { [weak self] result in From fbc6c31866e1f87688e4817966a0f30bec73405d Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:51:13 +0900 Subject: [PATCH 51/52] =?UTF-8?q?[#234]=20=EB=A7=88=EC=9D=B4-=EB=82=B4?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=9D=84=EC=9A=B8=20=EB=95=8C=20?= =?UTF-8?q?UI=20=EC=A4=91=EB=B3=B5=20=EC=B2=98=EB=A6=AC=EB=90=98=EB=8A=94?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NoDepartmentSheetViewController.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift index acfd2e9e..5b0ff577 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/NoDepartmentSheetViewController.swift @@ -101,13 +101,18 @@ final class NoDepartmentSheetViewController: BaseViewController { // "내 정보" 탭으로 전환 tabContainer.setTab(index: 2) - if let myNav = tabContainer.getNavController(at: 2) { - // 닉네임/학과 설정 화면으로 푸시 - dismiss(animated: true) { - myNav.pushViewController(SetNickNameViewController(), animated: true) - } - } else { + guard let myNav = tabContainer.getNavController(at: 2) else { dismiss(animated: true) + return + } + + dismiss(animated: true) { + if let existingVC = myNav.viewControllers.first(where: { $0 is SetNickNameViewController }) { + myNav.popToViewController(existingVC, animated: true) + } else { + let setNickNameVC = SetNickNameViewController() + myNav.pushViewController(setNickNameVC, animated: true) + } } } } From a77518cd6a5410175268592a675da226af484f89 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:09:29 +0900 Subject: [PATCH 52/52] =?UTF-8?q?[#234]=20iOS=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=AC=B8=EB=B2=95=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/Map/View/MainMapView.swift | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift index 11ed6cb9..3fa9068d 100644 --- a/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift +++ b/EATSSU/App/Sources/Presentation/Map/View/MainMapView.swift @@ -39,26 +39,56 @@ final class MainMapView: BaseUIView { toggleBackgroundView.backgroundColor = .white addSubview(toggleBackgroundView) + let titleFont = EATSSUDesignFontFamily.Pretendard.semiBold.font(size: 14) + // 전체 버튼 wholeButton.setTitle("전체", for: .normal) - wholeButton.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.semiBold.font(size: 14) + wholeButton.titleLabel?.font = titleFont wholeButton.layer.cornerRadius = 14 wholeButton.clipsToBounds = true wholeButton.backgroundColor = .clear wholeButton.setTitleColor(.label, for: .normal) + if #available(iOS 15.0, *) { + var cfg = wholeButton.configuration ?? .plain() + cfg.title = "전체" + cfg.baseForegroundColor = .label + cfg.baseBackgroundColor = .clear + cfg.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12) + cfg.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { inAttrs in + var out = inAttrs + out.font = titleFont + return out + } + wholeButton.configuration = cfg + } else { + wholeButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) + } // 내 제휴 버튼 myOnlyButton.setTitle("내 제휴", for: .normal) - myOnlyButton.titleLabel?.font = EATSSUDesignFontFamily.Pretendard.semiBold.font(size: 14) - myOnlyButton.setTitleColor(.label, for: .normal) - myOnlyButton.backgroundColor = .clear + myOnlyButton.titleLabel?.font = titleFont myOnlyButton.layer.cornerRadius = 14 myOnlyButton.clipsToBounds = true - myOnlyButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) + myOnlyButton.backgroundColor = .clear + myOnlyButton.setTitleColor(.label, for: .normal) + if #available(iOS 15.0, *) { + var cfg = myOnlyButton.configuration ?? .plain() + cfg.title = "내 제휴" + cfg.baseForegroundColor = .label + cfg.baseBackgroundColor = .clear + cfg.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12) + cfg.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { inAttrs in + var out = inAttrs + out.font = titleFont + return out + } + myOnlyButton.configuration = cfg + } else { + myOnlyButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) + } - // 초기 선택 상태 설정 + // 초기 선택 상태 selectWhole(true) - toggleBackgroundView.addSubview(wholeButton) toggleBackgroundView.addSubview(myOnlyButton) }