From d0c4e47713aa56016665144060cec3ea0a4f284f Mon Sep 17 00:00:00 2001 From: p2glet Date: Fri, 5 Dec 2025 00:34:13 +0900 Subject: [PATCH 01/34] =?UTF-8?q?fix/#273:=20=EC=83=81=EC=84=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=95=84=ED=84=B0=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=EC=A4=91..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DictionaryList/DictionaryType.swift | 14 ++- .../ItemDictionaryDetailViewController.swift | 8 +- .../MapDictionaryDetailViewController.swift | 96 +++++++++---------- ...onsterDictionaryDetailViewController.swift | 72 +++++++------- .../NpcDictionaryDetailViewController.swift | 52 +++++----- 5 files changed, 124 insertions(+), 118 deletions(-) diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift index 9fb29663..69e76d2d 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift @@ -41,19 +41,23 @@ public enum DictionaryType: CaseIterable { } } - public var detailSortedFilter: [SortType] { + public var detailTypes: [DetailType] { switch self { - case .item, .monster: + case .item: + return [ + .dropMonsterWithText + ] + case .monster: return [ - .mostDrop, .levelDESC, .levelASC + .appearMap, .dropItemWithText ] case .map: return [ - .mostAppear + .appearMonsterWithText, .appearNPC ] case .npc: return [ - .levelLowest, .levelHighest + .quest ] default: return [] diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift index c3a7c7e3..18e097f4 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift @@ -136,7 +136,8 @@ private extension ItemDictionaryDetailViewController { func setUpMonsterView() { guard let reactor = reactor, - let filter = reactor.currentState.type.detailSortedFilter.first else { return } + let detailType = reactor.currentState.type.detailTypes.first, + let filter = detailType.sortFilter.first else { return } monsterCardView.initFilter(firstFilter: filter) let monsters = reactor.currentState.monsters monsterCardView.reset() @@ -210,9 +211,10 @@ extension ItemDictionaryDetailViewController { .subscribe { owner, route in switch route { case .filter(let type): - let viewController = owner.sortedFactory.make(sortedOptions: type.detailSortedFilter, selectedIndex: owner.selectedIndex) { index in + guard let option = type.detailTypes.first else { return } + let viewController = owner.sortedFactory.make(sortedOptions: option.sortFilter, selectedIndex: owner.selectedIndex) { index in owner.selectedIndex = index - let selectedFilter = reactor.currentState.type.detailSortedFilter[index] + let selectedFilter = option.sortFilter[index] owner.monsterCardView.selectFilter(selectedType: selectedFilter) reactor.action.onNext(.selectFilter(selectedFilter)) } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift index 67881480..c09e84df 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift @@ -53,35 +53,35 @@ private extension MapDictionaryDetailViewController { } func setUpMonsterView() { - guard let reactor = reactor, - let filter = reactor.currentState.type.detailSortedFilter.first else { return } - appearMonsterView.initFilter(firstFilter: filter) - - let monsters = reactor.currentState.spawnMonsters - contentViews.append(appearMonsterView) - if monsters.isEmpty { - contentViews[1] = DetailEmptyView(type: .appearMonsterWithText) - } else { - contentViews[1] = appearMonsterView - for monster in monsters { - appearMonsterView.inject(input: DetailStackCardView.Input(type: .appearMonsterWithText, imageUrl: monster.imageUrl ?? "", mainText: monster.monsterName, subText: "Lv.\(monster.level ?? 0)")) - } - } +// guard let reactor = reactor, +// let filter = reactor.currentState.type.detailTypes.first else { return } +// appearMonsterView.initFilter(firstFilter: filter) +// +// let monsters = reactor.currentState.spawnMonsters +// contentViews.append(appearMonsterView) +// if monsters.isEmpty { +// contentViews[1] = DetailEmptyView(type: .appearMonsterWithText) +// } else { +// contentViews[1] = appearMonsterView +// for monster in monsters { +// appearMonsterView.inject(input: DetailStackCardView.Input(type: .appearMonsterWithText, imageUrl: monster.imageUrl ?? "", mainText: monster.monsterName, subText: "Lv.\(monster.level ?? 0)")) +// } +// } } func setUpNpcView() { - guard let reactor = reactor, let filter = reactor.currentState.type.detailSortedFilter.first else { return } - appearNpcView.initFilter(firstFilter: filter) - let npcs = reactor.currentState.npcs - contentViews.append(appearNpcView) - if npcs.isEmpty { - contentViews[2] = DetailEmptyView(type: .appearNPC) - } else { - contentViews[2] = appearNpcView - for npc in npcs { - appearNpcView.inject(input: DetailStackCardView.Input(type: .appearNPC, imageUrl: npc.iconUrl ?? "", mainText: npc.npcName)) - } - } +// guard let reactor = reactor, let filter = reactor.currentState.type.detailTypes.first else { return } +// appearNpcView.initFilter(firstFilter: filter) +// let npcs = reactor.currentState.npcs +// contentViews.append(appearNpcView) +// if npcs.isEmpty { +// contentViews[2] = DetailEmptyView(type: .appearNPC) +// } else { +// contentViews[2] = appearNpcView +// for npc in npcs { +// appearNpcView.inject(input: DetailStackCardView.Input(type: .appearNPC, imageUrl: npc.iconUrl ?? "", mainText: npc.npcName)) +// } +// } } func bindImageView() { @@ -175,27 +175,27 @@ extension MapDictionaryDetailViewController { ) .disposed(by: disposeBag) - rx.viewDidAppear - .take(1) - .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 - .withUnretained(self) - .subscribe { owner, route in - switch route { - case .filter(let type): - let viewController = owner.sortedFactory.make(sortedOptions: type.detailSortedFilter, selectedIndex: owner.selectedIndex) { index in - owner.selectedIndex = index - let selectedFilter = reactor.currentState.type.detailSortedFilter[index] - owner.appearMonsterView.selectFilter(selectedType: selectedFilter) - reactor.action.onNext(.selectFilter(selectedFilter)) - } - owner.tabBarController?.presentModal(viewController) - case .none: - break - case .detail(let type, let id): - let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) - owner.navigationController?.pushViewController(viewController, animated: true) - } - } - .disposed(by: disposeBag) +// rx.viewDidAppear +// .take(1) +// .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 +// .withUnretained(self) +// .subscribe { owner, route in +// switch route { +// case .filter(let type): +// let viewController = owner.sortedFactory.make(sortedOptions: type.detailTypes, selectedIndex: owner.selectedIndex) { index in +// owner.selectedIndex = index +// let selectedFilter = reactor.currentState.type.detailTypes[index] +// owner.appearMonsterView.selectFilter(selectedType: selectedFilter) +// reactor.action.onNext(.selectFilter(selectedFilter)) +// } +// owner.tabBarController?.presentModal(viewController) +// case .none: +// break +// case .detail(let type, let id): +// let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) +// owner.navigationController?.pushViewController(viewController, animated: true) +// } +// } +// .disposed(by: disposeBag) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift index c7fdca31..737a1b50 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift @@ -50,8 +50,8 @@ private extension MonsterDictionaryDetailViewController { func setUpMapView() { guard let reactor = reactor, - let filter = reactor.currentState.type.detailSortedFilter.first else { return } - appearMapView.initFilter(firstFilter: filter) + let filter = reactor.currentState.type.detailTypes.first else { return } +// appearMapView.initFilter(firstFilter: filter) let maps = reactor.currentState.spawnMaps contentViews.append(appearMapView) @@ -138,8 +138,8 @@ extension MonsterDictionaryDetailViewController { } private func bindViewState(reactor: Reactor) { - let selectedFilter = reactor.currentState.type.detailSortedFilter[selectedIndex] - dropItemView.selectFilter(selectedType: selectedFilter) +// let selectedFilter = reactor.currentState.type.detailTypes[selectedIndex] +// dropItemView.selectFilter(selectedType: selectedFilter) isBottomTabbarHidden = true @@ -175,38 +175,38 @@ extension MonsterDictionaryDetailViewController { }) .disposed(by: disposeBag) - rx.viewDidAppear - .take(1) - .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 - .withUnretained(self) - .subscribe { owner, route in - switch route { - case .filter(let type): - let selectedIndex = (type == .item) ? owner.dropItemSelectedIndex : owner.mapSelectedIntdex - - let viewController = owner.sortedFactory.make(sortedOptions: type.detailSortedFilter, selectedIndex: selectedIndex) { index in - if type == .item { - owner.dropItemSelectedIndex = index - let selectedFilter = type.detailSortedFilter[index] - owner.dropItemView.selectFilter(selectedType: selectedFilter) - reactor.action.onNext(.selectFilter(selectedFilter)) - - } else if type == .map { - owner.mapSelectedIntdex = index - let selectedFilter = type.detailSortedFilter[index] - owner.appearMapView.selectFilter(selectedType: selectedFilter) - } - owner.isBottomTabbarHidden = true - } - owner.tabBarController?.presentModal(viewController) - case let .detail(type: type, id: id): - let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) - owner.navigationController?.pushViewController(viewController, animated: true) - default: - break - } - } - .disposed(by: disposeBag) +// rx.viewDidAppear +// .take(1) +// .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 +// .withUnretained(self) +// .subscribe { owner, route in +// switch route { +// case .filter(let type): +// let selectedIndex = (type == .item) ? owner.dropItemSelectedIndex : owner.mapSelectedIntdex +// +// let viewController = owner.sortedFactory.make(sortedOptions: type.detailTypes, selectedIndex: selectedIndex) { index in +// if type == .item { +// owner.dropItemSelectedIndex = index +// let selectedFilter = type.detailTypes[index] +// owner.dropItemView.selectFilter(selectedType: selectedFilter) +// reactor.action.onNext(.selectFilter(selectedFilter)) +// +// } else if type == .map { +// owner.mapSelectedIntdex = index +// let selectedFilter = type.detailTypes[index] +// owner.appearMapView.selectFilter(selectedType: selectedFilter) +// } +// owner.isBottomTabbarHidden = true +// } +// owner.tabBarController?.presentModal(viewController) +// case let .detail(type: type, id: id): +// let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) +// owner.navigationController?.pushViewController(viewController, animated: true) +// default: +// break +// } +// } +// .disposed(by: disposeBag) bindBookmarkButton( buttonTap: mainView.bookmarkButton.rx.tap, diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift index abe46a33..f27f4ac4 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift @@ -106,32 +106,32 @@ extension NpcDictionaryDetailViewController { } private func bindViewState(reactor: Reactor) { - let selectedFilter = reactor.currentState.type.detailSortedFilter[selectedIndex] - questView.selectFilter(selectedType: selectedFilter) - isBottomTabbarHidden = true - - rx.viewDidAppear - .take(1) - .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 - .withUnretained(self) - .subscribe { owner, route in - switch route { - case .filter(let type): - let viewController = owner.sortedFactory.make(sortedOptions: type.detailSortedFilter, selectedIndex: owner.selectedIndex) { index in - owner.selectedIndex = index - let selectedFilter = reactor.currentState.type.detailSortedFilter[index] - owner.questView.selectFilter(selectedType: selectedFilter) - reactor.action.onNext(.selectFilter(selectedFilter)) - } - owner.tabBarController?.presentModal(viewController) - case .detail(type: let type, id: let id): - let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) - owner.navigationController?.pushViewController(viewController, animated: true) - default: - break - } - } - .disposed(by: disposeBag) + let selectedFilter = reactor.currentState.type.detailTypes[selectedIndex] +// questView.selectFilter(selectedType: selectedFilter) +// isBottomTabbarHidden = true +// +// rx.viewDidAppear +// .take(1) +// .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 +// .withUnretained(self) +// .subscribe { owner, route in +// switch route { +// case .filter(let type): +// let viewController = owner.sortedFactory.make(sortedOptions: type.detailTypes, selectedIndex: owner.selectedIndex) { index in +// owner.selectedIndex = index +// let selectedFilter = reactor.currentState.type.detailTypes[index] +// owner.questView.selectFilter(selectedType: selectedFilter) +// reactor.action.onNext(.selectFilter(selectedFilter)) +// } +// owner.tabBarController?.presentModal(viewController) +// case .detail(type: let type, id: let id): +// let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) +// owner.navigationController?.pushViewController(viewController, animated: true) +// default: +// break +// } +// } +// .disposed(by: disposeBag) reactor.state.map(\.npcDetailInfo) .distinctUntilChanged() From a27afa06058ed3cbbaaed7977405d984fbe71cc8 Mon Sep 17 00:00:00 2001 From: p2glet Date: Sun, 7 Dec 2025 21:51:48 +0900 Subject: [PATCH 02/34] =?UTF-8?q?fix/#273:=20=EC=83=81=EC=84=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=95=84=ED=84=B0=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?/=20=ED=83=AD=EB=B0=94=20=EC=9D=B4=EC=8A=88=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 --- .../DictionaryDetailItemResponseDTO.swift | 4 +- .../Endpoints/DictionaryDetailEndPoint.swift | 4 +- .../DictionaryDetailAPIRepositoryImpl.swift | 4 +- ...naryDetailMapSpawnMonsterUseCaseImpl.swift | 4 +- .../DictionaryList/DictionaryType.swift | 2 +- .../DictionaryDetailAPIRepository.swift | 2 +- ...ctionaryDetailMapSpawnMonsterUseCase.swift | 2 +- .../ModalPresentable/UIViewController+.swift | 69 +++++++++-- .../DictionaryDetailBaseViewController.swift | 4 +- .../ItemDictionaryDetailViewController.swift | 2 +- .../Map/MapDictionaryDetailReactor.swift | 16 +-- .../MapDictionaryDetailViewController.swift | 107 ++++++++++-------- .../MonsterDictionaryDetailReactor.swift | 13 ++- ...onsterDictionaryDetailViewController.swift | 88 +++++++------- .../NPC/NpcDictionaryDetailReactor.swift | 7 +- .../NpcDictionaryDetailViewController.swift | 56 ++++----- .../Quest/QuestDictionaryDetailReactor.swift | 72 +++++++++--- .../QuestDictionaryDetailViewController.swift | 29 +++-- .../DetailStackCardView.swift | 19 ++-- 19 files changed, 319 insertions(+), 185 deletions(-) diff --git a/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Item/DictionaryDetailItemResponseDTO.swift b/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Item/DictionaryDetailItemResponseDTO.swift index 75e93b06..656b43fa 100644 --- a/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Item/DictionaryDetailItemResponseDTO.swift +++ b/MLS/Data/Data/Network/DTO/DictionaryDetailDTO/Item/DictionaryDetailItemResponseDTO.swift @@ -5,7 +5,7 @@ public struct DictionaryDetailItemResponseDTO: Decodable { public let nameKr: String? public let nameEn: String? public let descriptionText: String? - public let imgUrl: String? + public let itemImageUrl: String? public let npcPrice: Int? public let itemType: String? public let categoryHierachy: CategoryHierachy? @@ -21,7 +21,7 @@ public struct DictionaryDetailItemResponseDTO: Decodable { nameKr: nameKr, nameEn: nameEn, descriptionText: descriptionText, - imgUrl: imgUrl, + imgUrl: itemImageUrl, npcPrice: npcPrice, itemType: itemType, categoryHierachy: categoryHierachy, diff --git a/MLS/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift b/MLS/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift index 42471e1c..5484e6a7 100644 --- a/MLS/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift +++ b/MLS/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift @@ -58,8 +58,8 @@ public enum DictionaryDetailEndPoint { } // Map 디테일 출현 몬스터 - public static func fetchMapDetailSpawnMonster(id: Int) -> ResponsableEndPoint<[DictionaryDetailMapSpawnMonsterResponseDTO]> { - return .init(baseURL: base, path: "/api/v1/maps/\(id)/monsters", method: .GET) + public static func fetchMapDetailSpawnMonster(id: Int, sort: [String]?) -> ResponsableEndPoint<[DictionaryDetailMapSpawnMonsterResponseDTO]> { + return .init(baseURL: base, path: "/api/v1/maps/\(id)/monsters", method: .GET, query: ["sort": sort?.joined(separator: ",")]) } // Map 디테일 출현 npc diff --git a/MLS/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift b/MLS/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift index 51d601c0..dfcf6bde 100644 --- a/MLS/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift @@ -69,8 +69,8 @@ public final class DictionaryDetailAPIRepositoryImpl: DictionaryDetailAPIReposit return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } } - public func fetchMapDetailSpawnMonster(id: Int) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> { - let endPoint = DictionaryDetailEndPoint.fetchMapDetailSpawnMonster(id: id) + public func fetchMapDetailSpawnMonster(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> { + let endPoint = DictionaryDetailEndPoint.fetchMapDetailSpawnMonster(id: id, sort: sort) return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.map {$0.toDomain()} } } diff --git a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCaseImpl.swift index 09f21ed0..eeea136d 100644 --- a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCaseImpl.swift @@ -7,7 +7,7 @@ public final class FetchDictionaryDetailMapSpawnMonsterUseCaseImpl: FetchDiction self.repository = repository } - public func execute(id: Int) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> { - return repository.fetchMapDetailSpawnMonster(id: id) + public func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> { + return repository.fetchMapDetailSpawnMonster(id: id, sort: sort) } } diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift index 69e76d2d..b35955cb 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift @@ -49,7 +49,7 @@ public enum DictionaryType: CaseIterable { ] case .monster: return [ - .appearMap, .dropItemWithText + .appearMapWithText, .dropItemWithText ] case .map: return [ diff --git a/MLS/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift b/MLS/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift index 2fb8e73a..d080c997 100644 --- a/MLS/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift +++ b/MLS/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift @@ -26,7 +26,7 @@ public protocol DictionaryDetailAPIRepository { // Map 디테일 상세정보 func fetchMapDetail(id: Int) -> Observable // Map 디테일 출현 몬스터 정보 - func fetchMapDetailSpawnMonster(id: Int) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> + func fetchMapDetailSpawnMonster(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> // Map 디테일 출현 Npc 정보 func fetchMapDetailNpc(id: Int) -> Observable<[DictionaryDetailMapNpcResponse]> } diff --git a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCase.swift b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCase.swift index 47865647..9921269b 100644 --- a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCase.swift +++ b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCase.swift @@ -1,5 +1,5 @@ import RxSwift public protocol FetchDictionaryDetailMapSpawnMonsterUseCase { - func execute(id: Int) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> + func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> } diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Utills/ModalPresentable/UIViewController+.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/ModalPresentable/UIViewController+.swift index 69819345..eec0382f 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/Utills/ModalPresentable/UIViewController+.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/Utills/ModalPresentable/UIViewController+.swift @@ -1,9 +1,11 @@ import UIKit import DesignSystem + import SnapKit private var modalWrapperKey: UInt8 = 0 +private var modalHideTabBarKey: UInt8 = 0 public extension UIViewController { @@ -12,17 +14,46 @@ public extension UIViewController { set { objc_setAssociatedObject(self, &modalWrapperKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } + private var modalHideTabBar: Bool { + get { (objc_getAssociatedObject(self, &modalHideTabBarKey) as? Bool) ?? false } + set { objc_setAssociatedObject(self, &modalHideTabBarKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + /// 커스텀 모달 프레젠트 - func presentModal(_ viewController: UIViewController & ModalPresentable) { + /// - Parameters: + /// - viewController: 표시할 모달 뷰컨 + /// - hideTabBar: 탭바를 숨길지 여부 (기본값: true) + func presentModal( + _ viewController: UIViewController & ModalPresentable, + hideTabBar: Bool = false + ) { let wrapper = ModalWrapperView(contentViewController: viewController, parent: self) + + // 이전 상태 초기화 + modalHideTabBar = false modalWrapperView = wrapper + + // 새 설정 적용 + modalHideTabBar = hideTabBar + view.addSubview(wrapper) wrapper.snp.makeConstraints { make in make.edges.equalToSuperview() } + // 필요 시 탭바 숨김 + if hideTabBar, let tabBarController = findTabBarController() { + tabBarController.setHidden(hidden: true, animated: false) + } + // present 애니메이션 - UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.85, initialSpringVelocity: 0.8, options: [.curveEaseOut]) { + UIView.animate( + withDuration: 0.5, + delay: 0, + usingSpringWithDamping: 0.85, + initialSpringVelocity: 0.8, + options: [.curveEaseOut] + ) { wrapper.dimView.alpha = 1 wrapper.containerView.transform = .identity DispatchQueue.main.async { @@ -36,16 +67,38 @@ public extension UIViewController { @objc internal func dismissCurrentModal() { guard let wrapper = modalWrapperView else { return } - wrapper.animateDismiss { - if let contentVC = wrapper.containerView.subviews.compactMap({ $0.next as? UIViewController }).first { - contentVC.willMove(toParent: nil) - contentVC.view.removeFromSuperview() - contentVC.removeFromParent() - } + let shouldKeepHidden = modalHideTabBar + let tabBarController = findTabBarController() + if shouldKeepHidden, let tabBarController { + tabBarController.setHidden(hidden: true, animated: false) + } + + UIView.animate(withDuration: 0.35, delay: 0, options: [.curveEaseInOut]) { + wrapper.dimView.alpha = 0 + wrapper.containerView.transform = CGAffineTransform(translationX: 0, y: 300) + } completion: { _ in wrapper.removeFromSuperview() self.modalWrapperView = nil + + // false인 경우 복원 + if !shouldKeepHidden, let tabBarController { + tabBarController.setHidden(hidden: false, animated: false) + } + + self.modalHideTabBar = false + } + } + + private func findTabBarController() -> BottomTabBarController? { + var parentVC: UIViewController? = self + while let current = parentVC { + if let tabBarController = current as? BottomTabBarController { + return tabBarController + } + parentVC = current.parent } + return nil } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index 2a4455e0..df5cab3a 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -48,7 +48,6 @@ class DictionaryDetailBaseViewController: BaseViewController { self.dictionaryDetailFactory = dictionaryDetailFactory mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) super.init() - isBottomTabbarHidden = true } @available(*, unavailable) @@ -140,9 +139,8 @@ extension DictionaryDetailBaseViewController { guard let self = self, let image = image else { return } self.mainView.imageView.image = image } - } else { - mainView.imageView.image = nil // Clear image if no URL + mainView.imageView.image = nil } mainView.imageContentView.backgroundColor = input.backgroundColor mainView.nameLabel.attributedText = .makeStyledString(font: .sub_l_m, text: input.name, color: .textColor) diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift index 18e097f4..dc46071c 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift @@ -218,7 +218,7 @@ extension ItemDictionaryDetailViewController { owner.monsterCardView.selectFilter(selectedType: selectedFilter) reactor.action.onNext(.selectFilter(selectedFilter)) } - owner.tabBarController?.presentModal(viewController) + owner.tabBarController?.presentModal(viewController, hideTabBar: true) case .none: break case .detail(let id): diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift index 91afbae1..4c2a96f7 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift @@ -6,11 +6,11 @@ public final class MapDictionaryDetailReactor: Reactor { // MARK: - Reactor public enum Route { case none - case filter(DictionaryType) + case filter([SortType]) case detail(type: DictionaryType, id: Int) } public enum Action { - case filterButtonTapped + case monsterFilterButtonTapped case viewWillAppear case toggleBookmark(Bool) case undoLastDeletedBookmark @@ -41,6 +41,9 @@ public final class MapDictionaryDetailReactor: Reactor { var spawnMonsters: [DictionaryDetailMapSpawnMonsterResponse] var npcs: [DictionaryDetailMapNpcResponse] var type: DictionaryType = .map + var monsterFilter: [SortType] { + type.detailTypes[0].sortFilter + } var id = 0 var isLogin = false var lastDeletedBookmark: DictionaryDetailMapResponse? @@ -84,13 +87,13 @@ public final class MapDictionaryDetailReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { - case .filterButtonTapped: - return Observable.just(.toNavigate(.filter(currentState.type))) + case .monsterFilterButtonTapped: + return Observable.just(.toNavigate(.filter(currentState.monsterFilter))) case .viewWillAppear: return .merge([ checkLoginUseCase.execute().map { .setLoginState($0) }, dictionaryDetailMapUseCase.execute(id: currentState.id).map {.setDetailData($0)}, - dictionaryDetailMapSpawnMonsterUseCase.execute(id: currentState.id).map {.setDetailSpawnMonsters($0)}, + dictionaryDetailMapSpawnMonsterUseCase.execute(id: currentState.id, sort: nil).map {.setDetailSpawnMonsters($0)}, dictionaryDetailMapNpcUseCase.execute(id: currentState.id).map {.setDetailNpc($0)} ]) case let .toggleBookmark(isSelected): @@ -111,8 +114,7 @@ public final class MapDictionaryDetailReactor: Reactor { ) ) case let .selectFilter(type): -// return dictionaryDetailMapSpawnMonsterUseCase.execute(id: currentState.id, sort: ["level", "asc"]).map { .setDetailSpawnMonsters($0) } - return .empty() + return dictionaryDetailMapSpawnMonsterUseCase.execute(id: currentState.id, sort: ["maxSpawnCount", "asc"]).map { .setDetailSpawnMonsters($0) } case .undoLastDeletedBookmark: guard let lastDeleted = currentState.lastDeletedBookmark, let mapId = lastDeleted.mapId else { return .empty() } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift index c09e84df..c598bb29 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift @@ -53,35 +53,43 @@ private extension MapDictionaryDetailViewController { } func setUpMonsterView() { -// guard let reactor = reactor, -// let filter = reactor.currentState.type.detailTypes.first else { return } -// appearMonsterView.initFilter(firstFilter: filter) -// -// let monsters = reactor.currentState.spawnMonsters -// contentViews.append(appearMonsterView) -// if monsters.isEmpty { -// contentViews[1] = DetailEmptyView(type: .appearMonsterWithText) -// } else { -// contentViews[1] = appearMonsterView -// for monster in monsters { -// appearMonsterView.inject(input: DetailStackCardView.Input(type: .appearMonsterWithText, imageUrl: monster.imageUrl ?? "", mainText: monster.monsterName, subText: "Lv.\(monster.level ?? 0)")) -// } -// } + guard let reactor = reactor, + let filter = reactor.currentState.monsterFilter.first else { return } + appearMonsterView.initFilter(firstFilter: filter) + + appearMonsterView.reset() + let monsters = reactor.currentState.spawnMonsters + contentViews.append(appearMonsterView) + if monsters.isEmpty { + contentViews[1] = DetailEmptyView(type: .appearMonsterWithText) + } else { + contentViews[1] = appearMonsterView + for monster in monsters { + appearMonsterView.inject(input: DetailStackCardView.Input(type: .appearMonsterWithText, imageUrl: monster.imageUrl ?? "", mainText: monster.monsterName, subText: "Lv.\(monster.level ?? 0)", additionalText: { + if let count = monster.maxSpawnCount { + return "\(count)마리" + } else { + return "??마리" + } + }())) + } + } } func setUpNpcView() { -// guard let reactor = reactor, let filter = reactor.currentState.type.detailTypes.first else { return } -// appearNpcView.initFilter(firstFilter: filter) -// let npcs = reactor.currentState.npcs -// contentViews.append(appearNpcView) -// if npcs.isEmpty { -// contentViews[2] = DetailEmptyView(type: .appearNPC) -// } else { -// contentViews[2] = appearNpcView -// for npc in npcs { -// appearNpcView.inject(input: DetailStackCardView.Input(type: .appearNPC, imageUrl: npc.iconUrl ?? "", mainText: npc.npcName)) -// } -// } + guard let reactor = reactor else { return } + + let npcs = reactor.currentState.npcs + appearNpcView.reset() + contentViews.append(appearNpcView) + if npcs.isEmpty { + contentViews[2] = DetailEmptyView(type: .appearNPC) + } else { + contentViews[2] = appearNpcView + for npc in npcs { + appearNpcView.inject(input: DetailStackCardView.Input(type: .appearNPC, imageUrl: npc.iconUrl ?? "", mainText: npc.npcName)) + } + } } func bindImageView() { @@ -96,6 +104,7 @@ private extension MapDictionaryDetailViewController { let url = reactor.currentState.mapDetailInfo.mapUrl else { return } let viewController = PinchMapViewController(imageUrl: url) viewController.modalPresentationStyle = .overFullScreen + owner.isBottomTabbarHidden = true self.present(viewController, animated: true) }) .disposed(by: disposeBag) @@ -117,7 +126,7 @@ extension MapDictionaryDetailViewController { .disposed(by: disposeBag) appearMonsterView.filterButton.rx.tap - .map { Reactor.Action.filterButtonTapped } + .map { Reactor.Action.monsterFilterButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -175,27 +184,27 @@ extension MapDictionaryDetailViewController { ) .disposed(by: disposeBag) -// rx.viewDidAppear -// .take(1) -// .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 -// .withUnretained(self) -// .subscribe { owner, route in -// switch route { -// case .filter(let type): -// let viewController = owner.sortedFactory.make(sortedOptions: type.detailTypes, selectedIndex: owner.selectedIndex) { index in -// owner.selectedIndex = index -// let selectedFilter = reactor.currentState.type.detailTypes[index] -// owner.appearMonsterView.selectFilter(selectedType: selectedFilter) -// reactor.action.onNext(.selectFilter(selectedFilter)) -// } -// owner.tabBarController?.presentModal(viewController) -// case .none: -// break -// case .detail(let type, let id): -// let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) -// owner.navigationController?.pushViewController(viewController, animated: true) -// } -// } -// .disposed(by: disposeBag) + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 + .withUnretained(self) + .subscribe { owner, route in + switch route { + case .filter(let sort): + let viewController = owner.sortedFactory.make(sortedOptions: sort, selectedIndex: owner.selectedIndex) { index in + owner.selectedIndex = index + let selectedFilter = reactor.currentState.monsterFilter[index] + owner.appearMonsterView.selectFilter(selectedType: selectedFilter) + reactor.action.onNext(.selectFilter(selectedFilter)) + } + owner.tabBarController?.presentModal(viewController, hideTabBar: true) + case .none: + break + case .detail(let type, let id): + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) + owner.navigationController?.pushViewController(viewController, animated: true) + } + } + .disposed(by: disposeBag) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift index b8315205..69943a4d 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift @@ -6,7 +6,7 @@ public final class MonsterDictionaryDetailReactor: Reactor { // MARK: - Type public enum Route { case none - case filter(DictionaryType) + case filter(type: DictionaryType, sort: [SortType]) case detail(type: DictionaryType, id: Int) } @@ -50,8 +50,15 @@ public final class MonsterDictionaryDetailReactor: Reactor { evasionRate: 0, mesoDropAmount: nil, mesoDropRate: nil, typeEffectiveness: nil, bookmarkId: nil ) - var dropItems = [DictionaryDetailMonsterDropItemResponse]() var spawnMaps = [DictionaryDetailMonsterMapResponse]() + var dropItems = [DictionaryDetailMonsterDropItemResponse]() + var mapFilter: [SortType] { + type.detailTypes[0].sortFilter + } + + var itemFilter: [SortType] { + type.detailTypes[1].sortFilter + } var infos = [Info]() var isLogin = false var lastDeletedBookmark: DictionaryDetailMonsterResponse? @@ -88,7 +95,7 @@ public final class MonsterDictionaryDetailReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { case let .filterButtonTapped(type): - return .just(.toNavigate(.filter(type))) + return .just(.toNavigate(.filter(type: type, sort: type == .map ? currentState.mapFilter : currentState.itemFilter))) case .viewWillAppear: return .merge([ diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift index 737a1b50..0363a686 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift @@ -50,10 +50,12 @@ private extension MonsterDictionaryDetailViewController { func setUpMapView() { guard let reactor = reactor, - let filter = reactor.currentState.type.detailTypes.first else { return } -// appearMapView.initFilter(firstFilter: filter) + let filter = reactor.currentState.mapFilter.first else { return } + + appearMapView.initFilter(firstFilter: filter) let maps = reactor.currentState.spawnMaps + appearMapView.reset() contentViews.append(appearMapView) if maps.isEmpty { contentViews[1] = DetailEmptyView(type: .appearMap) @@ -67,7 +69,13 @@ private extension MonsterDictionaryDetailViewController { imageUrl: map.iconUrl, mainText: map.mapName, subText: map.regionName, - additionalText: "\(map.maxSpawnCount ?? 0)마리" + additionalText: { + if let count = map.maxSpawnCount { + return "\(count)마리" + } else { + return "??마리" + } + }() ) ) } @@ -75,8 +83,12 @@ private extension MonsterDictionaryDetailViewController { } func setUpDropItemView() { - guard let reactor = reactor else { return } + guard let reactor = reactor, + let filter = reactor.currentState.itemFilter.first else { return } + + dropItemView.initFilter(firstFilter: filter) let items = reactor.currentState.dropItems + dropItemView.reset() contentViews.append(dropItemView) // 드롭아이템 @@ -138,11 +150,6 @@ extension MonsterDictionaryDetailViewController { } private func bindViewState(reactor: Reactor) { -// let selectedFilter = reactor.currentState.type.detailTypes[selectedIndex] -// dropItemView.selectFilter(selectedType: selectedFilter) - - isBottomTabbarHidden = true - reactor.state.map(\.monsterDetailInfo) .distinctUntilChanged() .observe(on: MainScheduler.instance) @@ -175,38 +182,37 @@ extension MonsterDictionaryDetailViewController { }) .disposed(by: disposeBag) -// rx.viewDidAppear -// .take(1) -// .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 -// .withUnretained(self) -// .subscribe { owner, route in -// switch route { -// case .filter(let type): -// let selectedIndex = (type == .item) ? owner.dropItemSelectedIndex : owner.mapSelectedIntdex -// -// let viewController = owner.sortedFactory.make(sortedOptions: type.detailTypes, selectedIndex: selectedIndex) { index in -// if type == .item { -// owner.dropItemSelectedIndex = index -// let selectedFilter = type.detailTypes[index] -// owner.dropItemView.selectFilter(selectedType: selectedFilter) -// reactor.action.onNext(.selectFilter(selectedFilter)) -// -// } else if type == .map { -// owner.mapSelectedIntdex = index -// let selectedFilter = type.detailTypes[index] -// owner.appearMapView.selectFilter(selectedType: selectedFilter) -// } -// owner.isBottomTabbarHidden = true -// } -// owner.tabBarController?.presentModal(viewController) -// case let .detail(type: type, id: id): -// let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) -// owner.navigationController?.pushViewController(viewController, animated: true) -// default: -// break -// } -// } -// .disposed(by: disposeBag) + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 + .withUnretained(self) + .subscribe { owner, route in + switch route { + case .filter(let type, let sort): + let selectedIndex = (type == .item) ? owner.dropItemSelectedIndex : owner.mapSelectedIntdex + + let viewController = owner.sortedFactory.make(sortedOptions: sort, selectedIndex: selectedIndex) { index in + if type == .item { + owner.dropItemSelectedIndex = index + let selectedFilter = sort[index] + owner.dropItemView.selectFilter(selectedType: selectedFilter) + reactor.action.onNext(.selectFilter(selectedFilter)) + } else if type == .map { + owner.mapSelectedIntdex = index + let selectedFilter = sort[index] + owner.appearMapView.selectFilter(selectedType: selectedFilter) + reactor.action.onNext(.selectFilter(selectedFilter)) + } + } + owner.tabBarController?.presentModal(viewController, hideTabBar: true) + case let .detail(type: type, id: id): + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) + owner.navigationController?.pushViewController(viewController, animated: true) + default: + break + } + } + .disposed(by: disposeBag) bindBookmarkButton( buttonTap: mainView.bookmarkButton.rx.tap, diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift index 8d42be45..2b409282 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift @@ -6,7 +6,7 @@ public final class NpcDictionaryDetailReactor: Reactor { // MARK: - Route public enum Route { case none - case filter(DictionaryType) + case filter([SortType]) case detail(type: DictionaryType, id: Int) } @@ -38,6 +38,9 @@ public final class NpcDictionaryDetailReactor: Reactor { var type: DictionaryType = .npc var maps: [DictionaryDetailMonsterMapResponse] var quests: [DictionaryDetailNpcQuestResponse] + var questFilter: [SortType] { + type.detailTypes[0].sortFilter + } var id: Int var isLogin = false var lastDeletedBookmark: DictionaryDetailNpcResponse? @@ -81,7 +84,7 @@ public final class NpcDictionaryDetailReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { case .filterButtonTapped: - return .just(.toNavigate(.filter(currentState.type))) + return .just(.toNavigate(.filter(currentState.questFilter))) case .viewWillAppear: return .merge([ checkLoginUseCase.execute().map { .setLoginState($0) }, diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift index f27f4ac4..b6f94410 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift @@ -37,6 +37,7 @@ private extension NpcDictionaryDetailViewController { func setUpMapView() { guard let reactor = reactor else { return } let maps = reactor.currentState.maps + appearMapView.reset() contentViews.append(appearMapView) if maps.isEmpty { @@ -56,8 +57,13 @@ private extension NpcDictionaryDetailViewController { } func setUpQuestView() { - guard let reactor = reactor else { return } + guard let reactor = reactor, + let filter = reactor.currentState.questFilter.first else { return } + + questView.initFilter(firstFilter: filter) + let quests = reactor.currentState.quests + questView.reset() contentViews.append(questView) if quests.isEmpty { // 퀘스트 @@ -106,32 +112,28 @@ extension NpcDictionaryDetailViewController { } private func bindViewState(reactor: Reactor) { - let selectedFilter = reactor.currentState.type.detailTypes[selectedIndex] -// questView.selectFilter(selectedType: selectedFilter) -// isBottomTabbarHidden = true -// -// rx.viewDidAppear -// .take(1) -// .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 -// .withUnretained(self) -// .subscribe { owner, route in -// switch route { -// case .filter(let type): -// let viewController = owner.sortedFactory.make(sortedOptions: type.detailTypes, selectedIndex: owner.selectedIndex) { index in -// owner.selectedIndex = index -// let selectedFilter = reactor.currentState.type.detailTypes[index] -// owner.questView.selectFilter(selectedType: selectedFilter) -// reactor.action.onNext(.selectFilter(selectedFilter)) -// } -// owner.tabBarController?.presentModal(viewController) -// case .detail(type: let type, id: let id): -// let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) -// owner.navigationController?.pushViewController(viewController, animated: true) -// default: -// break -// } -// } -// .disposed(by: disposeBag) + rx.viewDidAppear + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } // 값이 바뀔때만 이벤트 받음 + .withUnretained(self) + .subscribe { owner, route in + switch route { + case .filter(let type): + let viewController = owner.sortedFactory.make(sortedOptions: type, selectedIndex: owner.selectedIndex) { index in + owner.selectedIndex = index + let selectedFilter = type[index] + owner.questView.selectFilter(selectedType: selectedFilter) + reactor.action.onNext(.selectFilter(selectedFilter)) + } + owner.tabBarController?.presentModal(viewController, hideTabBar: true) + case .detail(type: let type, id: let id): + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) + owner.navigationController?.pushViewController(viewController, animated: true) + default: + break + } + } + .disposed(by: disposeBag) reactor.state.map(\.npcDetailInfo) .distinctUntilChanged() diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift index 61395512..32a3e1b1 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift @@ -3,6 +3,17 @@ import DomainInterface import ReactorKit public final class QuestDictionaryDetailReactor: Reactor { + enum QuestType { + case previous + case current + case next + } + + struct QuestInfo: Equatable { + let quest: Quest + let type: QuestType + } + public enum Route { case none case filter(DictionaryType) @@ -30,6 +41,7 @@ public final class QuestDictionaryDetailReactor: Reactor { var id: Int var detailInfo: DictionaryDetailQuestResponse var linkedQuestInfo: DictionaryDetailQuestLinkedQuestsResponse + var totalQuest: [QuestInfo] var isLogin = false var lastDeletedBookmark: DictionaryDetailQuestResponse? } @@ -75,7 +87,7 @@ public final class QuestDictionaryDetailReactor: Reactor { allowedJobs: nil, bookmarkId: nil ), - linkedQuestInfo: .init(previousQuests: nil, nextQuests: nil) + linkedQuestInfo: .init(previousQuests: nil, nextQuests: nil), totalQuest: [] ) } @@ -122,18 +134,10 @@ public final class QuestDictionaryDetailReactor: Reactor { ]) ) case let .questTapped(index): - if let previous = currentState.linkedQuestInfo.previousQuests, !previous.isEmpty { - if index == 0, let questId = previous.first?.questId { - return .just(.toNavigate(.detail(id: questId))) - } else if index == 1, let next = currentState.linkedQuestInfo.nextQuests?.first?.questId { - return .just(.toNavigate(.detail(id: next))) - } - } else { - if let next = currentState.linkedQuestInfo.nextQuests, index == 0, let questId = next.first?.questId { - return .just(.toNavigate(.detail(id: questId))) - } - } - return .empty() + let tappedQuestInfo = currentState.totalQuest[index] + guard let id = tappedQuestInfo.quest.questId, + tappedQuestInfo.type != .current else { return .empty() } + return .just(.toNavigate(.detail(id: id))) } } @@ -142,15 +146,55 @@ public final class QuestDictionaryDetailReactor: Reactor { switch mutation { case let .setDetailData(data): newState.detailInfo = data + newState.totalQuest = mergeTotalQuests( + detailInfo: data, + linkedInfo: state.linkedQuestInfo + ) case let .setLinkedQuests(data): newState.linkedQuestInfo = data + newState.totalQuest = mergeTotalQuests( + detailInfo: state.detailInfo, + linkedInfo: data + ) case let .setLoginState(isLogin): newState.isLogin = isLogin case let .setLastDeletedBookmark(data): newState.lastDeletedBookmark = data - case .toNavigate(let route): + case let .toNavigate(route): newState.route = route } return newState } } + +extension QuestDictionaryDetailReactor { + private func mergeTotalQuests( + detailInfo: DictionaryDetailQuestResponse, + linkedInfo: DictionaryDetailQuestLinkedQuestsResponse + ) -> [QuestInfo] { + var quests: [QuestInfo] = [] + + if let previous = linkedInfo.previousQuests { + let mapped = previous.map { QuestInfo(quest: $0, type: .previous) } + quests.append(contentsOf: mapped) + } + + if let currentId = detailInfo.questId { + let currentQuest = Quest( + questId: currentId, + name: detailInfo.nameKr ?? "", + minLevel: detailInfo.minLevel, + maxLevel: detailInfo.maxLevel, + iconUrl: detailInfo.iconUrl + ) + quests.append(QuestInfo(quest: currentQuest, type: .current)) + } + + if let next = linkedInfo.nextQuests { + let mapped = next.map { QuestInfo(quest: $0, type: .next) } + quests.append(contentsOf: mapped) + } + + return quests + } +} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift index d19806c1..7313a073 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift @@ -98,17 +98,26 @@ private extension QuestDictionaryDetailViewController { func setUpQuestView() { guard let reactor = reactor else { return } - let quests = reactor.currentState.linkedQuestInfo + let quests = reactor.currentState.totalQuest + + linkedQuestView.reset() contentViews.append(linkedQuestView) - if let previousQuests = quests.previousQuests, let nextQuests = quests.nextQuests { - if previousQuests.isEmpty, nextQuests.isEmpty { - contentViews[1] = DetailEmptyView(type: .quest) - } else { - contentViews[1] = linkedQuestView - for quest in previousQuests + nextQuests { - linkedQuestView.inject(input: DetailStackCardView.Input(type: .linkedQuest, imageUrl: quest.iconUrl ?? "", mainText: quest.name, subText: "수락 Lv.\(quest.minLevel ?? 0)") + + if quests.isEmpty { + contentViews[1] = DetailEmptyView(type: .quest) + } else { + contentViews[1] = linkedQuestView + + for data in quests { + linkedQuestView.inject( + input: DetailStackCardView.Input( + type: .linkedQuest, + imageUrl: data.quest.iconUrl ?? "", + mainText: data.quest.name, + subText: "수락 Lv.\(data.quest.minLevel ?? 0)", + questType: data.type ) - } + ) } } } @@ -146,7 +155,7 @@ extension QuestDictionaryDetailViewController { }) .disposed(by: disposeBag) - reactor.state.map(\.linkedQuestInfo) + reactor.state.map(\.totalQuest) .distinctUntilChanged() .observe(on: MainScheduler.instance) .withUnretained(self) diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackCardView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackCardView.swift index cdca16a2..3d567b67 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackCardView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackCardView.swift @@ -119,8 +119,7 @@ extension DetailStackCardView { var subText: String? // 오른쪽 텍스트 var additionalText: String? - // 퀘스트 판별을 위한 인덱스 0: preQuest, 1: currentQuest, 2: nextQuest - var questIndex: Int? + var questType: QuestDictionaryDetailReactor.QuestType? init( type: DetailType, @@ -128,14 +127,14 @@ extension DetailStackCardView { mainText: String?, subText: String? = nil, additionalText: String? = nil, - questIndex: Int? = nil + questType: QuestDictionaryDetailReactor.QuestType? = nil ) { self.type = type self.imageUrl = imageUrl self.mainText = mainText self.subText = subText self.additionalText = additionalText - self.questIndex = questIndex + self.questType = questType } } @@ -144,7 +143,6 @@ extension DetailStackCardView { setFilter(isHidden: input.type.sortFilter.isEmpty) let cardView = CardList() cardViews.append(cardView) - let currentIndex = cardViews.count - 1 let spacer = UIView() addArrangedSubview(cardView) @@ -182,10 +180,10 @@ extension DetailStackCardView { case .appearMap, .appearNPC, .quest: cardView.setType(type: .detailStack) case .linkedQuest: - switch input.questIndex { - case 0: + switch input.questType { + case .previous: cardView.setType(type: .detailStackBadge(.preQuest)) - case 1: + case .current: cardView.setType(type: .detailStackBadge(.currentQuest)) default: cardView.setType(type: .detailStackBadge(.nextQuest)) @@ -196,7 +194,10 @@ extension DetailStackCardView { cardView.rx.tapGesture() .when(.recognized) - .map { _ in currentIndex } + .map { [weak self] _ -> Int in + guard let self = self else { return 0 } + return self.cardViews.firstIndex(of: cardView) ?? 0 + } .bind(to: tapSubject) .disposed(by: disposeBag) } From deb6fd166e591c418ec9dfc3dfe3032833c5d0c4 Mon Sep 17 00:00:00 2001 From: p2glet Date: Mon, 8 Dec 2025 00:17:35 +0900 Subject: [PATCH 03/34] =?UTF-8?q?fix/#273:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0(=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A6=AC?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Endpoints/BookmarkEndPoint.swift | 4 +- .../DictionaryMainResponse.swift | 2 +- .../DictionaryListCell.swift | 3 + .../BookmarkListViewController.swift | 3 +- .../CollectionDetailViewController.swift | 2 +- .../DictionaryDetailBaseViewController.swift | 14 +- .../DictionaryDetailFactoryImpl.swift | 14 +- .../ItemDictionaryDetailViewController.swift | 3 +- .../MapDictionaryDetailViewController.swift | 3 +- ...onsterDictionaryDetailViewController.swift | 3 +- .../NpcDictionaryDetailViewController.swift | 3 +- .../QuestDictionaryDetailViewController.swift | 3 +- .../DictionaryListReactor.swift | 162 ++++++++---------- .../DictionaryListViewController.swift | 69 +++++--- .../DictionaryDetailFactory.swift | 4 +- 15 files changed, 155 insertions(+), 137 deletions(-) diff --git a/MLS/Data/Data/Network/Endpoints/BookmarkEndPoint.swift b/MLS/Data/Data/Network/Endpoints/BookmarkEndPoint.swift index 81cd6f7f..e81ffa9f 100644 --- a/MLS/Data/Data/Network/Endpoints/BookmarkEndPoint.swift +++ b/MLS/Data/Data/Network/Endpoints/BookmarkEndPoint.swift @@ -3,7 +3,7 @@ import DomainInterface public enum BookmarkEndPoint { static let base = "https://api.mapleland.kro.kr" - public static func setBookmark(body: Encodable) -> EndPoint { + public static func setBookmark(body: Encodable) -> ResponsableEndPoint<[BookmarkDTO]> { .init( baseURL: base, path: "/api/v1/bookmarks", @@ -12,7 +12,7 @@ public enum BookmarkEndPoint { ) } - public static func deleteBookmark(bookmarkId: Int) -> EndPoint { + public static func deleteBookmark(bookmarkId: Int) -> ResponsableEndPoint<[BookmarkDTO]> { .init( baseURL: base, path: "/api/v1/bookmarks/\(bookmarkId)", diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift index 145358a1..96b147a1 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift @@ -16,7 +16,7 @@ public struct DictionaryMainItemResponse: Equatable { public let imageUrl: String? public let level: Int? public let type: DictionaryItemType - public let bookmarkId: Int? + public var bookmarkId: Int? public init(id: Int, name: String, imageUrl: String?, level: Int?, type: DictionaryItemType, bookmarkId: Int?) { self.id = id diff --git a/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift b/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift index 21c7d543..a060c4d4 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift @@ -98,6 +98,9 @@ public extension DictionaryListCell { self?.onBookmarkTapped?(isSelected) } } + func updateBookmarkState(isBookmarked: Bool) { + cellView.setSelected(isSelected: isBookmarked) + } } public extension DictionaryItemType { diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift index 4c3f4973..997458a2 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift @@ -7,6 +7,7 @@ import DesignSystem import DictionaryFeatureInterface import ReactorKit +import RxCocoa import RxSwift public final class BookmarkListViewController: BaseViewController, View { @@ -178,7 +179,7 @@ extension BookmarkListViewController { break } case .detail(let type, let id): - let viewcontroller = owner.dictionaryDetailFactory.make(type: type, id: id) + let viewcontroller = owner.dictionaryDetailFactory.make(type: type, id: id, bookmarkRelay: nil) owner.navigationController?.pushViewController(viewcontroller, animated: true) case .login: let viewcontroller = owner.loginFactory.make(exitRoute: .pop) diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailViewController.swift index 6ff006bf..e7b7b81b 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionDetail/CollectionDetailViewController.swift @@ -197,7 +197,7 @@ extension CollectionDetailViewController { }) owner.presentModal(viewController) case .detail(let type, let id): - let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id, bookmarkRelay: nil) owner.navigationController?.pushViewController(viewController, animated: true) default: break diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index df5cab3a..2df83c67 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -16,6 +16,7 @@ class DictionaryDetailBaseViewController: BaseViewController { private var didSelectInitialTab = false var selectedIndex = 0 + var bookmarkRelay: PublishRelay<(Int, Bool)>? /// 각 탭에 해당하는 콘텐츠 뷰들을 담는 배열 public var contentViews: [UIView] = [] { @@ -40,13 +41,14 @@ class DictionaryDetailBaseViewController: BaseViewController { // 타입설정 public var type: DictionaryItemType - public init(type: DictionaryItemType, bookmarkModalFactory: BookmarkModalFactory, loginFactory: LoginFactory, dictionaryDetailFactory: DictionaryDetailFactory, appCoordinator: AppCoordinatorProtocol) { + public init(type: DictionaryItemType, bookmarkModalFactory: BookmarkModalFactory, loginFactory: LoginFactory, dictionaryDetailFactory: DictionaryDetailFactory, appCoordinator: AppCoordinatorProtocol, bookmarkRelay: PublishRelay<(Int, Bool)>?) { self.type = type self.bookmarkModalFactory = bookmarkModalFactory self.loginFactory = loginFactory self.appCoordinator = appCoordinator self.dictionaryDetailFactory = dictionaryDetailFactory mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) + self.bookmarkRelay = bookmarkRelay super.init() } @@ -237,11 +239,6 @@ extension DictionaryDetailBaseViewController { } } - // 북마크 버튼 클릭 시 - func updateBookmarkButton(isBookmarked: Bool) { - // TODO: 북마크 버튼 누르면 이벤트 발생 - } - func didSelectMenuTab(index: Int) { // 인덱스 유효성 검사 guard index < contentViews.count else { return } @@ -257,6 +254,7 @@ extension DictionaryDetailBaseViewController { buttonTap: ControlEvent, currentItem: Observable, isLogin: @escaping () -> Bool, + id: @escaping (T) -> Int, imageUrl: @escaping (T) -> String?, backgroundColor: UIColor, isBookmarked: @escaping (T) -> Bool, @@ -282,9 +280,12 @@ extension DictionaryDetailBaseViewController { ) return } + + let itemId = id(item) if isBookmarked(item) { toggleBookmark(true) + self.bookmarkRelay?.accept((itemId, false)) SnackBarFactory.createSnackBar( type: .delete, imageUrl: imageUrl(item), @@ -295,6 +296,7 @@ extension DictionaryDetailBaseViewController { ) } else { toggleBookmark(false) + self.bookmarkRelay?.accept((itemId, true)) SnackBarFactory.createSnackBar( type: .normal, imageUrl: imageUrl(item), diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift index 7148e978..ebab42d5 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift @@ -4,6 +4,8 @@ import BookmarkFeatureInterface import DictionaryFeatureInterface import DomainInterface +import RxCocoa + public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { private let loginFactory: () -> LoginFactory private let bookmarkModalFactory: BookmarkModalFactory @@ -69,7 +71,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { self.dictionaryDetailFactory = dictionaryDetailFactory } - public func make(type: DictionaryType, id: Int) -> BaseViewController { + public func make(type: DictionaryType, id: Int, bookmarkRelay: PublishRelay<(Int, Bool)>?) -> BaseViewController { var viewController = BaseViewController() switch type { case .total: @@ -77,7 +79,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { case .collection: break case .item: - viewController = ItemDictionaryDetailViewController(type: .item, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator()) + viewController = ItemDictionaryDetailViewController(type: .item, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) let reactor = ItemDictionaryDetailReactor( dictionaryDetailItemUseCase: dictionaryDetailItemUseCase, dictionaryDetailItemDropMonsterUseCase: dictionaryDetailItemDropMonsterUseCase, @@ -89,7 +91,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { viewController.reactor = reactor } case .monster: - viewController = MonsterDictionaryDetailViewController(type: .monster, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator()) + viewController = MonsterDictionaryDetailViewController(type: .monster, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) let reactor = MonsterDictionaryDetailReactor( dictionaryDetailMonsterUseCase: dictionaryDetailMonsterUseCase, dictionaryDetailMonsterDropItemUseCase: dictionaryDetailMonsterDropItemUseCase, @@ -102,6 +104,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { viewController.reactor = reactor } case .map: + viewController = MapDictionaryDetailViewController(type: .map, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) let reactor = MapDictionaryDetailReactor( dictionaryDetailMapUseCase: dictionaryDetailMapUseCase, dictionaryDetailMapSpawnMonsterUseCase: dictionaryDetailMapSpawnMonsterUseCase, @@ -110,11 +113,11 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { setBookmarkUseCase: setBookmarkUseCase, id: id ) - viewController = MapDictionaryDetailViewController(type: .map, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator()) if let viewController = viewController as? MapDictionaryDetailViewController { viewController.reactor = reactor } case .npc: + viewController = NpcDictionaryDetailViewController(type: .npc, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) let reactor = NpcDictionaryDetailReactor( dictionaryDetailNpcUseCase: dictionaryDetailNpcUseCase, dictionaryDetailNpcQuestUseCase: dictionaryDetailNpcQuestUseCase, @@ -123,12 +126,11 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { setBookmarkUseCase: setBookmarkUseCase, id: id ) - viewController = NpcDictionaryDetailViewController(type: .npc, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator()) if let viewController = viewController as? NpcDictionaryDetailViewController { viewController.reactor = reactor } case .quest: - viewController = QuestDictionaryDetailViewController(type: .quest, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator()) + viewController = QuestDictionaryDetailViewController(type: .quest, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) let reactor = QuestDictionaryDetailReactor( dictionaryDetailQuestUseCase: dictionaryDetailQuestUseCase, dictionaryDetailQuestLinkedQuestUseCase: dictionaryDetailQuestLinkedQuestsUseCase, diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift index dc46071c..3f544aba 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift @@ -222,7 +222,7 @@ extension ItemDictionaryDetailViewController { case .none: break case .detail(let id): - let viewController = owner.dictionaryDetailFactory.make(type: .monster, id: id) + let viewController = owner.dictionaryDetailFactory.make(type: .monster, id: id, bookmarkRelay: self.bookmarkRelay) owner.navigationController?.pushViewController(viewController, animated: true) } } @@ -232,6 +232,7 @@ extension ItemDictionaryDetailViewController { buttonTap: mainView.bookmarkButton.rx.tap, currentItem: reactor.state.map { $0.itemDetailInfo }, isLogin: { reactor.currentState.isLogin }, + id: { _ in reactor.currentState.itemDetailInfo.itemId ?? 0 }, imageUrl: { $0.imgUrl }, backgroundColor: type.backgroundColor, isBookmarked: { $0.bookmarkId != nil }, diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift index c598bb29..da01bef7 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift @@ -175,6 +175,7 @@ extension MapDictionaryDetailViewController { buttonTap: mainView.bookmarkButton.rx.tap, currentItem: reactor.state.map { $0.mapDetailInfo }, isLogin: { reactor.currentState.isLogin }, + id: { _ in reactor.currentState.mapDetailInfo.mapId ?? 0 }, imageUrl: { $0.mapUrl }, backgroundColor: type.backgroundColor, isBookmarked: { $0.bookmarkId != nil }, @@ -201,7 +202,7 @@ extension MapDictionaryDetailViewController { case .none: break case .detail(let type, let id): - let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id, bookmarkRelay: self.bookmarkRelay) owner.navigationController?.pushViewController(viewController, animated: true) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift index 0363a686..c386e132 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift @@ -206,7 +206,7 @@ extension MonsterDictionaryDetailViewController { } owner.tabBarController?.presentModal(viewController, hideTabBar: true) case let .detail(type: type, id: id): - let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id, bookmarkRelay: self.bookmarkRelay) owner.navigationController?.pushViewController(viewController, animated: true) default: break @@ -218,6 +218,7 @@ extension MonsterDictionaryDetailViewController { buttonTap: mainView.bookmarkButton.rx.tap, currentItem: reactor.state.map { $0.monsterDetailInfo }, isLogin: { reactor.currentState.isLogin }, + id: { _ in reactor.currentState.monsterDetailInfo.monsterId }, imageUrl: { $0.imageUrl }, backgroundColor: type.backgroundColor, isBookmarked: { $0.bookmarkId != nil }, diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift index b6f94410..b5aef1f7 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift @@ -127,7 +127,7 @@ extension NpcDictionaryDetailViewController { } owner.tabBarController?.presentModal(viewController, hideTabBar: true) case .detail(type: let type, id: let id): - let viewController = owner.dictionaryDetailFactory.make(type: type, id: id) + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id, bookmarkRelay: self.bookmarkRelay) owner.navigationController?.pushViewController(viewController, animated: true) default: break @@ -167,6 +167,7 @@ extension NpcDictionaryDetailViewController { buttonTap: mainView.bookmarkButton.rx.tap, currentItem: reactor.state.map { $0.npcDetailInfo }, isLogin: { reactor.currentState.isLogin }, + id: { _ in reactor.currentState.npcDetailInfo.npcId }, imageUrl: { $0.iconUrlDetail }, backgroundColor: type.backgroundColor, isBookmarked: { $0.bookmarkId != nil }, diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift index 7313a073..f61dc04e 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift @@ -168,6 +168,7 @@ extension QuestDictionaryDetailViewController { buttonTap: mainView.bookmarkButton.rx.tap, currentItem: reactor.state.map { $0.detailInfo }, isLogin: { reactor.currentState.isLogin }, + id: { _ in reactor.currentState.detailInfo.questId ?? 0 }, imageUrl: { $0.iconUrl }, backgroundColor: type.backgroundColor, isBookmarked: { $0.bookmarkId != nil }, @@ -184,7 +185,7 @@ extension QuestDictionaryDetailViewController { .subscribe { owner, route in switch route { case .detail(let id): - let viewController = owner.dictionaryDetailFactory.make(type: .quest, id: id) + let viewController = owner.dictionaryDetailFactory.make(type: .quest, id: id, bookmarkRelay: self.bookmarkRelay) owner.navigationController?.pushViewController(viewController, animated: true) default: break diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift index 621a2c83..252b3947 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift @@ -28,8 +28,6 @@ open class DictionaryListReactor: Reactor { // MARK: - Mutation public enum Mutation { case setListItem(DictionaryMainResponse) - case setFilterMonsterItem(DictionaryMainResponse) - case setFilterItemsItem(DictionaryMainResponse) case showSortFilter case showFilter case setSort(String) @@ -40,6 +38,7 @@ open class DictionaryListReactor: Reactor { case setLastDeletedBookmark(DictionaryMainItemResponse?) case setJobId([Int]) case setCategoryId([Int]) + case updateBookmarkState(id: Int, isSelected: Bool) } // MARK: - State @@ -145,13 +144,59 @@ open class DictionaryListReactor: Reactor { case .undoLastDeletedBookmark: return handleUndoLastDeletedBookmark() - case .itemFilterOptionSelected(let results): + case let .itemFilterOptionSelected(results): return handleItemFilterOptionSelected(results: results) } } - // MARK: - Fetch - private func fetchList( + // MARK: - Reduce + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .showSortFilter: + newState.route = .sort(newState.type) + case .showFilter: + newState.route = .filter(newState.type) + case let .setListItem(items): + newState.totalCounts = items.totalElements + if newState.currentPage == 0 { + newState.listItems = items.contents + } else { + newState.listItems.append(contentsOf: items.contents) + } + case let .setSort(sort): + newState.sort = sort + case let .setFilter(startLevel, endLevel): + newState.startLevel = startLevel + newState.endLevel = endLevel + case .setCurrentPage: + newState.currentPage += 1 + case .initPage: + newState.currentPage = 0 + case let .setLastDeletedBookmark(item): + newState.lastDeletedBookmark = item + case let .setLoginState(isLogin): + newState.isLogin = isLogin + case let .setJobId(id): + newState.jobId = id + case let .setCategoryId(id): + newState.categoryIds = id + case let .updateBookmarkState(id, isSelected): + if let index = newState.listItems.firstIndex(where: { $0.id == id }) { + if isSelected { + newState.listItems[index].bookmarkId = newState.listItems[index].bookmarkId ?? -1 + } else { + newState.listItems[index].bookmarkId = nil + } + } + } + return newState + } +} + +// MARK: - Methods +private extension DictionaryListReactor { + func fetchList( sort: String?, startLevel: Int?, endLevel: Int?, @@ -219,64 +264,9 @@ open class DictionaryListReactor: Reactor { return .empty() } - return response.map { res in - if isFilter { - switch self.currentState.type { - case .monster: - return .setFilterMonsterItem(res) - case .item: - return .setFilterItemsItem(res) - default: - return .setListItem(res) - } - } else { - return .setListItem(res) - } - } + return response.map { .setListItem($0) } } - // MARK: - Reduce - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - switch mutation { - case let .setFilterMonsterItem(items), - let .setFilterItemsItem(items): - newState.listItems = items.contents - case .showSortFilter: - newState.route = .sort(newState.type) - case .showFilter: - newState.route = .filter(newState.type) - case let .setListItem(items): - newState.totalCounts = items.totalElements - if newState.currentPage == 0 { - newState.listItems = items.contents - } else { - newState.listItems.append(contentsOf: items.contents) - } - case let .setSort(sort): - newState.sort = sort - case let .setFilter(startLevel, endLevel): - newState.startLevel = startLevel - newState.endLevel = endLevel - case .setCurrentPage: - newState.currentPage += 1 - case .initPage: - newState.currentPage = 0 - case let .setLastDeletedBookmark(item): - newState.lastDeletedBookmark = item - case let .setLoginState(isLogin): - newState.isLogin = isLogin - case .setJobId(let id): - newState.jobId = id - case .setCategoryId(let id): - newState.categoryIds = id - } - return newState - } -} - -// MARK: - Methods -private extension DictionaryListReactor { func handleViewWillAppear() -> Observable { let loginState = checkLoginUseCase.execute() .map { Mutation.setLoginState($0) } @@ -290,26 +280,27 @@ private extension DictionaryListReactor { return .merge([loginState, fetchMutation]) } - func handleToggleBookmark(id: Int, isSelected: Bool) -> Observable { - guard let bookmarkItem = currentState.listItems.first(where: { $0.id == id }) else { return .empty() } - let bookmarkId = bookmarkItem.bookmarkId ?? 0 + private func handleToggleBookmark(id: Int, isSelected: Bool) -> Observable { + guard let index = currentState.listItems.firstIndex(where: { $0.id == id }) else { return .empty() } + let targetItem = currentState.listItems[index] - let saveDeletedMutation: Observable = isSelected - ? .just(.setLastDeletedBookmark(bookmarkItem)) - : .just(.setLastDeletedBookmark(nil)) + let saveDeleted = isSelected + ? Observable.just(Mutation.setLastDeletedBookmark(targetItem)) + : .empty() - return saveDeletedMutation.concat( - setBookmarkUseCase.execute( - bookmarkId: isSelected ? bookmarkId : id, - isBookmark: isSelected ? .delete : .set(bookmarkItem.type) - ) - .andThen( - Observable.concat([ - .just(.initPage), - fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel) - ]) - ) + let optimistic = Observable.just(Mutation.updateBookmarkState(id: id, isSelected: !isSelected)) + + let api = setBookmarkUseCase.execute( + bookmarkId: isSelected ? targetItem.bookmarkId ?? id : targetItem.id, + isBookmark: isSelected ? .delete : .set(targetItem.type) ) + .andThen(fetchList( + sort: currentState.sort, + startLevel: currentState.startLevel, + endLevel: currentState.endLevel + )) + + return .concat([saveDeleted, optimistic, api]) } func handleSortOptionSelected(sort: SortType) -> Observable { @@ -337,17 +328,16 @@ private extension DictionaryListReactor { func handleUndoLastDeletedBookmark() -> Observable { guard let lastDeleted = currentState.lastDeletedBookmark else { return .empty() } - return setBookmarkUseCase.execute( + + let optimistic = Observable.just(Mutation.updateBookmarkState(id: lastDeleted.id, isSelected: true)) + + let api = setBookmarkUseCase.execute( bookmarkId: lastDeleted.id, isBookmark: .set(lastDeleted.type) ) - .andThen( - Observable.concat([ - .just(.initPage), - fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel), - .just(.setLastDeletedBookmark(nil)) - ]) - ) + .andThen(Observable.just(Mutation.setLastDeletedBookmark(nil))) + + return .concat([optimistic, api]) } func handleItemFilterOptionSelected(results: [(String, String)]) -> Observable { diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift index e1b6330d..9ca5dbbb 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift @@ -23,6 +23,7 @@ public final class DictionaryListViewController: BaseViewController, View { private var selectedSortIndex = 0 public let itemCountRelay = PublishRelay() + private let bookmarkChangeRelay = PublishRelay<(Int, Bool)>() // MARK: - Components private var mainView: DictionaryListView @@ -124,15 +125,28 @@ extension DictionaryListViewController { .disposed(by: disposeBag) reactor.state.map(\.listItems) - .distinctUntilChanged() .observe(on: MainScheduler.instance) - .bind(onNext: { [weak self] item in - self?.mainView.listCollectionView.reloadData() - self?.mainView.emptyView.isHidden = !item.isEmpty - self?.mainView.listCollectionView.isHidden = item.isEmpty + .bind(onNext: { [weak self] items in + guard let self = self else { return } + self.mainView.emptyView.isHidden = !items.isEmpty + self.mainView.listCollectionView.isHidden = items.isEmpty // 보여줄 item이 없을 경우, 터치를 막는데 왜 막는건지? // 몬스터나 아이템 탭에서 필터링을 하다가 item이 없을 경우, 필터 버튼도 터치가 안되서 계속 item 없음 // self?.mainView.isUserInteractionEnabled = !item.isEmpty + let collectionView = self.mainView.listCollectionView + + let currentItems = collectionView.numberOfItems(inSection: 0) + if items.count == currentItems { + for cell in collectionView.visibleCells { + if let indexPath = collectionView.indexPath(for: cell), + indexPath.item < items.count, + let cell = cell as? DictionaryListCell { + cell.updateBookmarkState(isBookmarked: items[indexPath.item].bookmarkId != nil) + } + } + } else { + collectionView.reloadData() + } }) .disposed(by: disposeBag) @@ -195,6 +209,13 @@ extension DictionaryListViewController { owner.mainView.updateFilter(sortType: type.sortedFilter.first) }) .disposed(by: disposeBag) + + bookmarkChangeRelay + .observe(on: MainScheduler.instance) + .bind(onNext: { [weak self] id, isBookmarked in + self?.reactor?.action.onNext(.toggleBookmark(id, isBookmarked)) + }) + .disposed(by: disposeBag) } } @@ -218,7 +239,7 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi mainText: item.name, subText: subText, imageUrl: item.imageUrl ?? "", - isBookmarked: item.bookmarkId != nil, + isBookmarked: item.bookmarkId != nil ), indexPath: indexPath, collectionView: collectionView, @@ -232,20 +253,17 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi ctaText: "로그인 하기", cancelText: "취소", ctaAction: { - let viewController = self.loginFactory.make( - exitRoute: .pop) - self.navigationController?.pushViewController( - viewController, animated: true - ) + let vc = self.loginFactory.make(exitRoute: .pop) + self.navigationController?.pushViewController(vc, animated: true) }, cancelAction: nil ) return } - if item.bookmarkId != nil { - self.reactor?.action.onNext( - .toggleBookmark(item.id, isSelected)) + self.reactor?.action.onNext(.toggleBookmark(item.id, isSelected)) + + if isSelected { SnackBarFactory.createSnackBar( type: .delete, imageUrl: item.imageUrl, @@ -253,13 +271,10 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi text: "아이템을 북마크에서 삭제했어요.", buttonText: "되돌리기", buttonAction: { [weak self] in - self?.reactor?.action.onNext( - .undoLastDeletedBookmark) + self?.reactor?.action.onNext(.undoLastDeletedBookmark) } ) } else { - self.reactor?.action.onNext( - .toggleBookmark(item.id, isSelected)) SnackBarFactory.createSnackBar( type: .normal, imageUrl: item.imageUrl, @@ -271,22 +286,20 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi guard let self = self, let id = item.bookmarkId else { return } - let viewController = self.bookmarkModalFactory.make(bookmarkIds: [id], onComplete: { isAdd in + let vc = self.bookmarkModalFactory.make(bookmarkIds: [id]) { isAdd in if isAdd { ToastFactory.createToast( - message: - "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요." + message: "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요." ) } - }) - - viewController.modalPresentationStyle = .pageSheet - if let sheet = viewController.sheetPresentationController { + } + vc.modalPresentationStyle = .pageSheet + if let sheet = vc.sheetPresentationController { sheet.detents = [.medium(), .large()] sheet.prefersGrabberVisible = true sheet.preferredCornerRadius = 16 } - self.present(viewController, animated: true) + self.present(vc, animated: true) } } ) @@ -309,11 +322,11 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi switch reactor.currentState.type { case .total: guard let type = item.type.toDictionaryType else { return } - viewController = detailFactory.make(type: type, id: item.id) + viewController = detailFactory.make(type: type, id: item.id, bookmarkRelay: bookmarkChangeRelay) default: // 단일 타입일 경우 리액터 타입에 따라 처리 viewController = detailFactory.make( - type: reactor.currentState.type, id: item.id + type: reactor.currentState.type, id: item.id, bookmarkRelay: bookmarkChangeRelay ) } navigationController?.pushViewController(viewController, animated: true) diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeatureInterface/DictionaryDetailFactory.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeatureInterface/DictionaryDetailFactory.swift index 346f3b3e..4844a946 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeatureInterface/DictionaryDetailFactory.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeatureInterface/DictionaryDetailFactory.swift @@ -1,6 +1,8 @@ import BaseFeature import DomainInterface +import RxCocoa + public protocol DictionaryDetailFactory { - func make(type: DictionaryType, id: Int) -> BaseViewController + func make(type: DictionaryType, id: Int, bookmarkRelay: PublishRelay<(Int, Bool)>?) -> BaseViewController } From ca29c8e50df5773e8194cd39dd4a91120525b48b Mon Sep 17 00:00:00 2001 From: p2glet Date: Mon, 8 Dec 2025 00:56:55 +0900 Subject: [PATCH 04/34] =?UTF-8?q?fix/#273:=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9D=98=20Int=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=97=90=20numberFormmat=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BaseFeature/Utills/Extension/Int+.swift | 11 +++++++++++ .../Item/ItemDictionaryDetailViewController.swift | 9 ++++----- .../Monster/MonsterDictionaryDetailReactor.swift | 10 +++++----- .../Quest/QuestDictionaryDetailViewController.swift | 6 +++--- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Utills/Extension/Int+.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/Extension/Int+.swift index 5443d97d..26a74e1b 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/Utills/Extension/Int+.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/Utills/Extension/Int+.swift @@ -1,5 +1,16 @@ +import Foundation + extension Array where Element == Int { public func changeKoreanDate() -> String { return "\(self[0])년 \(self[1])월 \(self[2])일 \(self[3]):\(String(format: "%02d", self[4]))" } } + +extension Int { + var formatted: String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.locale = Locale(identifier: "ko_KR") + return formatter.string(from: NSNumber(value: self)) ?? "\(self)" + } +} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift index 3f544aba..b1d727c8 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift @@ -37,8 +37,7 @@ private extension ItemDictionaryDetailViewController { detailInfoView.descriptionLabel.text = infos.descriptionText ?? "" if let npcPrice = infos.npcPrice { - let formattedPrice = NumberFormatter.localizedString(from: NSNumber(value: npcPrice), number: .decimal) - detailInfoView.addInfo(mainText: "상점판매가", subText: "\(formattedPrice) 메소") + detailInfoView.addInfo(mainText: "상점판매가", subText: "\(npcPrice.formatted()) 메소") } if let availableJobs = infos.availableJobs { @@ -95,7 +94,7 @@ private extension ItemDictionaryDetailViewController { } if let attackSpeed = equipmentStats.attackSpeed, let attackSpeedDetails = equipmentStats.attackSpeedDetails { - detailInfoView.addInfo(mainText: "공격속도", subText: "\(attackSpeed) (\(attackSpeedDetails))") + detailInfoView.addInfo(mainText: "공격속도", subText: "\(attackSpeed.formatted()) (\(attackSpeedDetails))") } } @@ -128,7 +127,7 @@ private extension ItemDictionaryDetailViewController { for (title, value) in scrollMappings { if let value = value { let sign = value >= 0 ? "+" : "" - detailInfoView.addInfo(mainText: title, subText: "\(sign)\(value)") + detailInfoView.addInfo(mainText: title, subText: "\(sign)\(value.formatted())") } } } @@ -247,7 +246,7 @@ extension ItemDictionaryDetailViewController { private extension ItemDictionaryDetailViewController { func formatStatText(base: Int, min: Int?, max: Int?) -> String { if let min = min, let max = max { - return "\(base) [\(min)-\(max)]" + return "\(base.formatted()) [\(min.formatted())-\(max.formatted())]" } else { return "\(base)" } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift index 69943a4d..f88fd11a 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift @@ -165,11 +165,11 @@ public final class MonsterDictionaryDetailReactor: Reactor { newState.monsterDetailInfo = data var infos: [Info] = [] - infos.append(.init(name: "HP", desc: "\(data.hp)")) - infos.append(.init(name: "MP", desc: "\(data.mp)")) - infos.append(.init(name: "EXP", desc: "\(data.exp)")) - infos.append(.init(name: "물리방어력", desc: "\(data.physicalDefense)")) - infos.append(.init(name: "마법방어력", desc: "\(data.magicDefense)")) + infos.append(.init(name: "HP", desc: "\(data.hp.formatted())")) + infos.append(.init(name: "MP", desc: "\(data.mp.formatted())")) + infos.append(.init(name: "EXP", desc: "\(data.exp.formatted())")) + infos.append(.init(name: "물리방어력", desc: "\(data.physicalDefense.formatted())")) + infos.append(.init(name: "마법방어력", desc: "\(data.magicDefense.formatted())")) newState.infos = infos case let .setDetailDropItemData(data): diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift index f61dc04e..1f7ea027 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift @@ -75,13 +75,13 @@ private extension QuestDictionaryDetailViewController { // 보상 추가 - 메소,경험치, 인기도 if let meso = rewardInfos?.meso { - detailInfoView.addReward(mainText: DictionaryDetailText.meso, subText: "\(meso)") + detailInfoView.addReward(mainText: DictionaryDetailText.meso, subText: "\(meso.formatted())") } if let exp = rewardInfos?.exp { - detailInfoView.addReward(mainText: DictionaryDetailText.exp, subText: "\(exp)") + detailInfoView.addReward(mainText: DictionaryDetailText.exp, subText: "\(exp.formatted())") } if let pop = rewardInfos?.popularity { - detailInfoView.addReward(mainText: DictionaryDetailText.pop, subText: "\(pop)") + detailInfoView.addReward(mainText: DictionaryDetailText.pop, subText: "\(pop.formatted())") } if let rewardItems = rewardItemInfos { for info in rewardItems { From 823d0857ddae924f24dfb1d3e5688ae1b2fdec5c Mon Sep 17 00:00:00 2001 From: p2glet Date: Mon, 8 Dec 2025 01:17:36 +0900 Subject: [PATCH 05/34] =?UTF-8?q?fix/#273:=20=ED=80=98=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=EA=B1=B4=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B4=EB=8F=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DictionaryList/DictionaryType.swift | 2 +- .../Quest/QuestDictionaryDetailReactor.swift | 7 ++- .../QuestDictionaryDetailViewController.swift | 49 +++++++++---------- .../DetailStackInfoView.swift | 20 ++++---- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift index b35955cb..51d441f5 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift @@ -1,4 +1,4 @@ -public enum DictionaryType: CaseIterable { +public enum DictionaryType: String, CaseIterable { case total case collection case item diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift index 32a3e1b1..42a0835f 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift @@ -17,7 +17,7 @@ public final class QuestDictionaryDetailReactor: Reactor { public enum Route { case none case filter(DictionaryType) - case detail(id: Int) + case detail(type: DictionaryType, id: Int) } public enum Action { @@ -25,6 +25,7 @@ public final class QuestDictionaryDetailReactor: Reactor { case toggleBookmark(Bool) case undoLastDeletedBookmark case questTapped(index: Int) + case infoTapped(type: DictionaryType, id: Int) } public enum Mutation { @@ -137,7 +138,9 @@ public final class QuestDictionaryDetailReactor: Reactor { let tappedQuestInfo = currentState.totalQuest[index] guard let id = tappedQuestInfo.quest.questId, tappedQuestInfo.type != .current else { return .empty() } - return .just(.toNavigate(.detail(id: id))) + return .just(.toNavigate(.detail(type: .quest, id: id))) + case let .infoTapped(type: type, id: id): + return .just(.toNavigate(.detail(type: type, id: id))) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift index 1f7ea027..c53bb310 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift @@ -41,13 +41,26 @@ private extension QuestDictionaryDetailViewController { if let requirements = detailInfos.requirements { for requirement in requirements { if let quantity = requirement.quantity { - if let name = requirement.itemName ?? requirement.monsterName { - detailInfoView.addCondition( - mainText: name, - subText: "\(quantity)", - clickable: true, - onTap: { self.presentAlert() } - ) + if let name = requirement.itemName ?? requirement.monsterName, + let type = DictionaryType(rawValue: requirement.requirementType ?? "") { + + if let id = type == .item ? requirement.itemId : requirement.monsterId { + detailInfoView.addCondition( + mainText: name, + subText: "\(quantity)", + clickable: true, + onTap: { [weak reactor] in + reactor?.action.onNext(.infoTapped(type: type, id: id)) + } + ) + } else { + detailInfoView.addCondition( + mainText: name, + subText: "\(quantity)", + clickable: false, + onTap: {} + ) + } } } } @@ -184,8 +197,8 @@ extension QuestDictionaryDetailViewController { .withUnretained(self) .subscribe { owner, route in switch route { - case .detail(let id): - let viewController = owner.dictionaryDetailFactory.make(type: .quest, id: id, bookmarkRelay: self.bookmarkRelay) + case let .detail(type, id): + let viewController = owner.dictionaryDetailFactory.make(type: type, id: id, bookmarkRelay: self.bookmarkRelay) owner.navigationController?.pushViewController(viewController, animated: true) default: break @@ -194,21 +207,3 @@ extension QuestDictionaryDetailViewController { .disposed(by: disposeBag) } } - -// MARK: - 임시Alert -extension QuestDictionaryDetailViewController { - func presentAlert() { - let alert = UIAlertController(title: "알림", message: "페이지 이동Alert", preferredStyle: .alert) - - let confirmAction = UIAlertAction(title: "확인", style: .default) { _ in - print("확인 버튼 클릭됨") - } - - let cancelAction = UIAlertAction(title: "취소", style: .cancel) - - alert.addAction(confirmAction) - alert.addAction(cancelAction) - - present(alert, animated: true) - } -} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift index 4ec00849..601bffcb 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift @@ -3,11 +3,11 @@ import UIKit import DesignSystem import DomainInterface +import RxGesture +import RxSwift import SnapKit final class DetailStackInfoView: UIStackView { - var onTap: (() -> Void)? // 외부에서 넘겨받을 콜백 - // MARK: - Type private enum Constant { static let descriptionCornerRadius: CGFloat = 16 @@ -21,6 +21,9 @@ final class DetailStackInfoView: UIStackView { static let titleLeadingInset: CGFloat = 16 } + // MARK: - Properties + private let disposeBag = DisposeBag() + // MARK: - Components // 상세정보 스택 뷰 속 설명 글 var descriptionLabel: UILabel = { @@ -238,10 +241,13 @@ extension DetailStackInfoView { mainLabel.attributedText = .makeStyledUnderlinedString(font: .sub_m_sb, text: mainText) mainLabel.isUserInteractionEnabled = true - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - mainLabel.addGestureRecognizer(tapGesture) - self.onTap = onTap // 저장해두기 + mainLabel.rx.tapGesture() + .when(.recognized) + .bind { _ in + onTap?() + } + .disposed(by: disposeBag) rowStackView.addArrangedSubview(clickableStack) } else { @@ -270,8 +276,4 @@ extension DetailStackInfoView { make.height.equalTo(Constant.dividerHeight) } } - - @objc private func handleTap() { - onTap?() - } } From 2684112ad2a18b9ccbe6f0cdb67bc704b7a80069 Mon Sep 17 00:00:00 2001 From: p2glet Date: Mon, 8 Dec 2025 02:41:27 +0900 Subject: [PATCH 06/34] =?UTF-8?q?fix/#273:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B9=84=ED=9A=8C=EC=9B=90=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MyPageFeature/Main/MyPageMainReactor.swift | 15 ++++++++++++++- .../Main/MyPageMainViewController.swift | 6 +++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainReactor.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainReactor.swift index 89617b49..514303ad 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainReactor.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainReactor.swift @@ -45,6 +45,15 @@ public final class MyPageMainReactor: Reactor { "약관 및 정책" } } + + var requiresLogin: Bool { + switch self { + case .setAlarm, .setCharacterInfo: + return true + default: + return false + } + } } // MARK: - Route @@ -109,7 +118,11 @@ public final class MyPageMainReactor: Reactor { return .just(.toNavigate(.login)) } case .menuItemTapped(let menu): - return .just(.toNavigate(menu.route)) + if currentState.profile == nil, menu.requiresLogin { + return .just(.toNavigate(.login)) + } else { + return .just(.toNavigate(menu.route)) + } case .viewWillAppear: return fetchProfileUseCase.execute() .map { .setProfile($0) } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainViewController.swift index b8a09526..aa9bc141 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainViewController.swift @@ -227,7 +227,11 @@ extension MyPageMainViewController: UICollectionViewDelegate, UICollectionViewDa let item = reactor.currentState.menus[indexPath.section - 1][indexPath.row - 1] switch item { case .setCharacterInfo(let .some(profile)): - cell.inject(input: MyPageListCell.Input(title: profile.jobName, isHeader: false, addLevel: profile.level)) + if let level = profile.level { + cell.inject(input: MyPageListCell.Input(title: profile.jobName, isHeader: false, addLevel: profile.level)) + } else { + cell.inject(input: MyPageListCell.Input(title: item.description, isHeader: false)) + } default: cell.inject(input: MyPageListCell.Input(title: item.description, isHeader: false)) } From 5368e20d7fc5c97071b1d1450e2af12339704baa Mon Sep 17 00:00:00 2001 From: p2glet Date: Mon, 8 Dec 2025 06:03:08 +0900 Subject: [PATCH 07/34] =?UTF-8?q?fix/#273:=20=EB=8F=84=EA=B0=90=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EA=B0=B1=EC=8B=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20/=20=EB=AA=AC=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=20=ED=95=84=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DictionaryMainResponse.swift | 2 +- .../Interface/EmptyViewState.swift | 57 ++++++++++++++ .../DictionaryListReactor.swift | 77 +++++++++++++++---- .../DictionaryListViewController.swift | 21 ++--- .../Views/FilterLevelSectionView.swift | 14 ++-- .../Views/FilterSlider.swift | 8 ++ .../MonsterFilterBottomSheetReactor.swift | 6 +- ...nsterFilterBottomSheetViewController.swift | 23 ++++-- 8 files changed, 166 insertions(+), 42 deletions(-) create mode 100644 MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift index 96b147a1..a6e74562 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift @@ -1,7 +1,7 @@ public struct DictionaryMainResponse { public let totalPages: Int public let totalElements: Int - public let contents: [DictionaryMainItemResponse] + public var contents: [DictionaryMainItemResponse] public init(totalPages: Int, totalElements: Int, contents: [DictionaryMainItemResponse]) { self.totalPages = totalPages diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift b/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift new file mode 100644 index 00000000..7df0bbd3 --- /dev/null +++ b/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift @@ -0,0 +1,57 @@ +// +//public enum ViewType: Equatable { +// case dictionary(ViewState) +// case bookmark(ViewState) +// +// public enum ViewState { +// case login(data: Bool) +// case logout(data: Bool) +// } +// +// var mainText: String? { +// switch self { +// case let .bookmark(state): +// switch state { +// case let .login(data): +// return data ? nil : "아직 아무것도 없어요!" +// case let .logout(data): +// return "컬렉션은 로그인 후 이용 가능해요!" +// } +// case let .dictionary(state): +// switch state { +// case let .login(data): +// return data ? nil : "검색 결과가 없어요" +// case let .logout(data): +// return data ? nil : "검색 결과가 없어요" +// } +// } +// } +// +// var subText: String? { +// switch self { +// case let .bookmark(state): +// switch state { +// case let .login(data): +// return data ? nil : "아직 아무것도 없어요!" +// case let .logout(data): +// return "자주 보는 정보, 검색 없이 바로 확인 할 수 있어요." +// } +// case .dictionary(_): +// return nil +// } +// } +// +// var buttonText: String? { +// switch self { +// case let .bookmark(state): +// switch state { +// case let .login(data): +// return data ? nil : "북마크하러 가기" +// case let .logout(data): +// return "로그인하러 가기" +// } +// case .dictionary(_): +// return nil +// } +// } +//} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift index 252b3947..36910642 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift @@ -27,7 +27,7 @@ open class DictionaryListReactor: Reactor { // MARK: - Mutation public enum Mutation { - case setListItem(DictionaryMainResponse) + case setListItem(DictionaryMainResponse, updateBookmarkOnly: Bool = false) case showSortFilter case showFilter case setSort(String) @@ -59,6 +59,7 @@ open class DictionaryListReactor: Reactor { var isLogin: Bool var lastDeletedBookmark: DictionaryMainItemResponse? + var isBookmarkUpdateOnly: Bool = false } public var initialState: State @@ -137,8 +138,7 @@ open class DictionaryListReactor: Reactor { return fetchList( sort: currentState.sort, startLevel: currentState.startLevel ?? 1, - endLevel: currentState.endLevel ?? 200, - isFilter: true + endLevel: currentState.endLevel ?? 200 ) case .undoLastDeletedBookmark: @@ -157,12 +157,27 @@ open class DictionaryListReactor: Reactor { newState.route = .sort(newState.type) case .showFilter: newState.route = .filter(newState.type) - case let .setListItem(items): + case let .setListItem(items, updateBookmarkOnly): + newState.isBookmarkUpdateOnly = updateBookmarkOnly newState.totalCounts = items.totalElements - if newState.currentPage == 0 { - newState.listItems = items.contents + if updateBookmarkOnly { + newState.listItems = newState.listItems.map { item in + if let updated = items.contents.first(where: { $0.id == item.id }) { + var copy = item + copy.bookmarkId = updated.bookmarkId ?? item.bookmarkId + return copy + } else { + return item + } + } } else { - newState.listItems.append(contentsOf: items.contents) + if newState.currentPage == 0 { + newState.listItems = items.contents + } else { + let existingIds = Set(newState.listItems.map { $0.id }) + let newItems = items.contents.filter { !existingIds.contains($0.id) } + newState.listItems.append(contentsOf: newItems) + } } case let .setSort(sort): newState.sort = sort @@ -200,7 +215,7 @@ private extension DictionaryListReactor { sort: String?, startLevel: Int?, endLevel: Int?, - isFilter: Bool = false + updateBookmarkOnly: Bool = false ) -> Observable { let response: Observable @@ -264,7 +279,26 @@ private extension DictionaryListReactor { return .empty() } - return response.map { .setListItem($0) } + return response.map { response in + var mergedItems: [DictionaryMainItemResponse] + + if updateBookmarkOnly { + mergedItems = self.currentState.listItems.map { item in + if let updated = response.contents.first(where: { $0.id == item.id }) { + var copy = item + copy.bookmarkId = updated.bookmarkId ?? item.bookmarkId + return copy + } else { + return item + } + } + var newResponse = response + newResponse.contents = mergedItems + return .setListItem(newResponse, updateBookmarkOnly: true) + } else { + return .setListItem(response) + } + } } func handleViewWillAppear() -> Observable { @@ -288,16 +322,21 @@ private extension DictionaryListReactor { ? Observable.just(Mutation.setLastDeletedBookmark(targetItem)) : .empty() - let optimistic = Observable.just(Mutation.updateBookmarkState(id: id, isSelected: !isSelected)) + let optimistic = Observable.just(Mutation.updateBookmarkState( + id: id, + isSelected: !isSelected + )) + // 서버 호출 후 bookmarkId 확정 let api = setBookmarkUseCase.execute( - bookmarkId: isSelected ? targetItem.bookmarkId ?? id : targetItem.id, + bookmarkId: isSelected ? targetItem.bookmarkId ?? targetItem.id : targetItem.id, isBookmark: isSelected ? .delete : .set(targetItem.type) ) .andThen(fetchList( sort: currentState.sort, startLevel: currentState.startLevel, - endLevel: currentState.endLevel + endLevel: currentState.endLevel, + updateBookmarkOnly: true )) return .concat([saveDeleted, optimistic, api]) @@ -320,8 +359,7 @@ private extension DictionaryListReactor { return self.fetchList( sort: self.currentState.sort, startLevel: startLevel, - endLevel: endLevel, - isFilter: true + endLevel: endLevel ) }) } @@ -335,7 +373,13 @@ private extension DictionaryListReactor { bookmarkId: lastDeleted.id, isBookmark: .set(lastDeleted.type) ) - .andThen(Observable.just(Mutation.setLastDeletedBookmark(nil))) + .andThen(fetchList( + sort: currentState.sort, + startLevel: currentState.startLevel, + endLevel: currentState.endLevel, + updateBookmarkOnly: true + )) + .concat(Observable.just(Mutation.setLastDeletedBookmark(nil))) return .concat([optimistic, api]) } @@ -353,8 +397,7 @@ private extension DictionaryListReactor { return self.fetchList( sort: self.currentState.sort, startLevel: self.currentState.startLevel, - endLevel: self.currentState.endLevel, - isFilter: true + endLevel: self.currentState.endLevel ) }) } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift index 9ca5dbbb..b0e5392f 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift @@ -24,6 +24,7 @@ public final class DictionaryListViewController: BaseViewController, View { private var selectedSortIndex = 0 public let itemCountRelay = PublishRelay() private let bookmarkChangeRelay = PublishRelay<(Int, Bool)>() + private var lastPagingTime: Date = .distantPast // MARK: - Components private var mainView: DictionaryListView @@ -130,13 +131,11 @@ extension DictionaryListViewController { guard let self = self else { return } self.mainView.emptyView.isHidden = !items.isEmpty self.mainView.listCollectionView.isHidden = items.isEmpty - // 보여줄 item이 없을 경우, 터치를 막는데 왜 막는건지? - // 몬스터나 아이템 탭에서 필터링을 하다가 item이 없을 경우, 필터 버튼도 터치가 안되서 계속 item 없음 - // self?.mainView.isUserInteractionEnabled = !item.isEmpty - let collectionView = self.mainView.listCollectionView - let currentItems = collectionView.numberOfItems(inSection: 0) - if items.count == currentItems { + if self.reactor?.currentState.currentPage == 0 || self.reactor?.currentState.isBookmarkUpdateOnly != true { + self.mainView.listCollectionView.reloadData() + } else { + let collectionView = self.mainView.listCollectionView for cell in collectionView.visibleCells { if let indexPath = collectionView.indexPath(for: cell), indexPath.item < items.count, @@ -144,8 +143,6 @@ extension DictionaryListViewController { cell.updateBookmarkState(isBookmarked: items[indexPath.item].bookmarkId != nil) } } - } else { - collectionView.reloadData() } }) .disposed(by: disposeBag) @@ -333,13 +330,17 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi } public func scrollViewDidScroll(_ scrollView: UIScrollView) { + let now = Date() + guard now.timeIntervalSince(lastPagingTime) > 0.5 else { return } + let offsetY = scrollView.contentOffset.y let contentHeight = scrollView.contentSize.height let height = scrollView.frame.size.height if offsetY > contentHeight - height - 100 { - reactor?.action.onNext(.setCurrentPage) // 페이지 올리고 - reactor?.action.onNext(.fetchList) // 해당 페이지로 데이터 불러오기 + lastPagingTime = now + reactor?.action.onNext(.setCurrentPage) + reactor?.action.onNext(.fetchList) } } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterLevelSectionView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterLevelSectionView.swift index 8faaa4f4..2b158d67 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterLevelSectionView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterLevelSectionView.swift @@ -19,7 +19,7 @@ public class FilterLevelSectionView: UIView { private var isEdit = false let leftInputBox: InputBox = { - let box = InputBox(label: "범위", placeHodler: "0") + let box = InputBox(label: "범위", placeHodler: "1") box.textField.keyboardType = .numberPad return box }() @@ -48,7 +48,7 @@ public class FilterLevelSectionView: UIView { private let lowerLabel: UILabel = { let label = UILabel() - label.attributedText = .makeStyledString(font: .b_s_r, text: "0", color: .neutral500) + label.attributedText = .makeStyledString(font: .b_s_r, text: "1", color: .neutral500) return label }() @@ -66,8 +66,8 @@ public class FilterLevelSectionView: UIView { public var disposeBag = DisposeBag() - public init(initialLowerValue: CGFloat = 0, initialUpperValue: CGFloat = 200) { - self.slider = FilterSlider(minimumValue: 0, maximumValue: 200, initialLowerValue: initialLowerValue, initialUpperValue: initialUpperValue) + public init(initialLowerValue: CGFloat = 1, initialUpperValue: CGFloat = 200) { + self.slider = FilterSlider(minimumValue: 1, maximumValue: 200, initialLowerValue: initialLowerValue, initialUpperValue: initialUpperValue) super.init(frame: .zero) addViews() setupConstraints() @@ -135,7 +135,7 @@ public extension FilterLevelSectionView { .subscribe { owner, value in guard !owner.isEdit else { return } let lowValue = Int(value) - owner.leftInputBox.textField.text = lowValue == 0 ? nil : "\(lowValue)" + owner.leftInputBox.textField.text = lowValue == 1 ? nil : "\(lowValue)" } .disposed(by: disposeBag) @@ -156,7 +156,7 @@ public extension FilterLevelSectionView { if let value = Double(text) { owner.slider.lowerValue = value } else { - owner.slider.lowerValue = 0 + owner.slider.lowerValue = 1 } } .disposed(by: disposeBag) @@ -181,7 +181,7 @@ public extension FilterLevelSectionView { .withUnretained(self) .debounce(.seconds(1), scheduler: MainScheduler.instance) .subscribe { owner, _ in - if let leftValue = Double(owner.leftInputBox.textField.text ?? "0"), let rightValue = Double(owner.rightInputBox.textField.text ?? "200") { + if let leftValue = Double(owner.leftInputBox.textField.text ?? "1"), let rightValue = Double(owner.rightInputBox.textField.text ?? "200") { if leftValue > rightValue { owner.isEdit = true owner.leftInputBox.textField.text = "\(Int(rightValue))" diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterSlider.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterSlider.swift index f4a799e1..e1d8c4fc 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterSlider.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterSlider.swift @@ -283,3 +283,11 @@ public class FilterSlider: UIControl { return super.hitTest(point, with: event) } } + +public extension FilterSlider { + func reset(lower: CGFloat, upper: CGFloat) { + lowerValueRelay.accept(boundValue(lower, lower: minimumValue, upper: maximumValue)) + upperValueRelay.accept(boundValue(upper, lower: minimumValue, upper: maximumValue)) + animateUpdate() + } +} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetReactor.swift index fcb0b1ca..a3d63887 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetReactor.swift @@ -6,12 +6,14 @@ public final class MonsterFilterBottomSheetReactor: Reactor { case none case dismiss case dismissWithLevelRange(start: Int, end: Int) + case clear } // MARK: - Reactor public enum Action { case cancelButtonTapped case applyButtonTapped(start: Int, end: Int) + case clearButtonTapped } public enum Mutation { @@ -27,7 +29,7 @@ public final class MonsterFilterBottomSheetReactor: Reactor { var disposeBag = DisposeBag() // MARK: - init - public init(startLevel: Int = 0, endLevel: Int = 200) { + public init(startLevel: Int = 1, endLevel: Int = 200) { self.initialState = State() } @@ -42,6 +44,8 @@ public final class MonsterFilterBottomSheetReactor: Reactor { } return .just(.navigateTo(route: .dismissWithLevelRange(start: start, end: end))) + case .clearButtonTapped: + return .just(.navigateTo(route: .clear)) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetViewController.swift index bb686555..2a038744 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetViewController.swift @@ -16,7 +16,7 @@ public final class MonsterFilterBottomSheetViewController: BaseViewController, M // MARK: - Properties public var disposeBag = DisposeBag() - var startLevel: CGFloat = 0 + var startLevel: CGFloat = 1 var endLevel: CGFloat = 200 public lazy var mainView = MonsterFilterBottomSheetView(lowerLevel: startLevel, upperLevel: endLevel) @@ -71,15 +71,24 @@ extension MonsterFilterBottomSheetViewController { .map { Reactor.Action.cancelButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) + + mainView.clearButton.rx.tap + .map { Reactor.Action.clearButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) mainView.applyButton.rx.tap .withUnretained(self) .compactMap { _, _ in - guard - let startText = self.mainView.levelRangeView.leftInputBox.textField.text, - let endText = self.mainView.levelRangeView.rightInputBox.textField.text, - let start = Int(startText), - let end = Int(endText) + let startText = (self.mainView.levelRangeView.leftInputBox.textField.text?.isEmpty == false) + ? self.mainView.levelRangeView.leftInputBox.textField.text! + : "1" + + let endText = (self.mainView.levelRangeView.rightInputBox.textField.text?.isEmpty == false) + ? self.mainView.levelRangeView.rightInputBox.textField.text! + : "200" + guard let start = Int(startText), + let end = Int(endText) else { return nil } @@ -101,6 +110,8 @@ extension MonsterFilterBottomSheetViewController { case .dismissWithLevelRange(let start, let end): owner.onFilterSelected?(start, end) owner.dismissCurrentModal() + case .clear: + owner.mainView.levelRangeView.slider.reset(lower: 1, upper: 200) default: break } From d65aaccf42c512ad04179b1ceea9548d114a5219 Mon Sep 17 00:00:00 2001 From: p2glet Date: Mon, 8 Dec 2025 22:35:33 +0900 Subject: [PATCH 08/34] =?UTF-8?q?fix/#273:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20-=20=EC=83=81=EC=84=B8=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=ED=99=94=20/=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20=ED=95=84=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{SharedView => Shared}/BaseListView.swift | 0 .../BaseFeature/Shared/BookmarkStore.swift | 28 ++++ .../CharacterInputView.swift | 0 .../DictionaryListReactor.swift | 140 +++++++----------- .../DictionaryListViewController.swift | 104 +++++++------ 5 files changed, 137 insertions(+), 135 deletions(-) rename MLS/Presentation/BaseFeature/BaseFeature/{SharedView => Shared}/BaseListView.swift (100%) create mode 100644 MLS/Presentation/BaseFeature/BaseFeature/Shared/BookmarkStore.swift rename MLS/Presentation/BaseFeature/BaseFeature/{SharedView => Shared}/CharacterInputView.swift (100%) diff --git a/MLS/Presentation/BaseFeature/BaseFeature/SharedView/BaseListView.swift b/MLS/Presentation/BaseFeature/BaseFeature/Shared/BaseListView.swift similarity index 100% rename from MLS/Presentation/BaseFeature/BaseFeature/SharedView/BaseListView.swift rename to MLS/Presentation/BaseFeature/BaseFeature/Shared/BaseListView.swift diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Shared/BookmarkStore.swift b/MLS/Presentation/BaseFeature/BaseFeature/Shared/BookmarkStore.swift new file mode 100644 index 00000000..af9543d4 --- /dev/null +++ b/MLS/Presentation/BaseFeature/BaseFeature/Shared/BookmarkStore.swift @@ -0,0 +1,28 @@ +//import RxSwift +//import RxCocoa +// +//public final class BookmarkStore { +// public static let shared = BookmarkStore() +// +// private let disposeBag = DisposeBag() +// +// // BehaviorRelay 사용 +// private let bookmarksRelay = BehaviorRelay<[Int: Bool]>(value: [:]) +// +// // 외부에서 Observable로 구독 가능 +// public var bookmarks: Observable<[Int: Bool]> { +// return bookmarksRelay.asObservable() +// } +// +// private init() {} +// +// public func setBookmark(id: Int, isBookmarked: Bool) { +// var current = bookmarksRelay.value +// current[id] = isBookmarked +// bookmarksRelay.accept(current) +// } +// +// public func isBookmarked(id: Int) -> Bool { +// return bookmarksRelay.value[id] ?? false +// } +//} diff --git a/MLS/Presentation/BaseFeature/BaseFeature/SharedView/CharacterInputView.swift b/MLS/Presentation/BaseFeature/BaseFeature/Shared/CharacterInputView.swift similarity index 100% rename from MLS/Presentation/BaseFeature/BaseFeature/SharedView/CharacterInputView.swift rename to MLS/Presentation/BaseFeature/BaseFeature/Shared/CharacterInputView.swift diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift index 36910642..2e9648e5 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift @@ -1,8 +1,11 @@ +import BaseFeature import DomainInterface + import ReactorKit import RxSwift -open class DictionaryListReactor: Reactor { +public final class DictionaryListReactor: Reactor { + // MARK: - Route public enum Route { case none @@ -12,7 +15,6 @@ open class DictionaryListReactor: Reactor { // MARK: - Action public enum Action { - case toggleBookmark(Int, Bool) case viewWillAppear case sortButtonTapped case filterButtonTapped @@ -21,8 +23,8 @@ open class DictionaryListReactor: Reactor { case itemFilterOptionSelected([(String, String)]) case setCurrentPage case fetchList - case fetchListFilter case undoLastDeletedBookmark + case toggleBookmark(id: Int, isSelected: Bool) } // MARK: - Mutation @@ -39,6 +41,7 @@ open class DictionaryListReactor: Reactor { case setJobId([Int]) case setCategoryId([Int]) case updateBookmarkState(id: Int, isSelected: Bool) + case updateBookmarkStates([Int: Bool]) // 새 Mutation: 여러 북마크 반영 } // MARK: - State @@ -108,42 +111,22 @@ open class DictionaryListReactor: Reactor { switch action { case .viewWillAppear: return handleViewWillAppear() - - case let .toggleBookmark(id, isSelected): - return handleToggleBookmark(id: id, isSelected: isSelected) - case .sortButtonTapped: return .just(.showSortFilter) - case .filterButtonTapped: return .just(.showFilter) - case let .sortOptionSelected(sort): return handleSortOptionSelected(sort: sort) - case let .filterOptionSelected(startLevel, endLevel): return handleFilterOptionSelected(startLevel: startLevel, endLevel: endLevel) - case .setCurrentPage: return .just(.setCurrentPage) - case .fetchList: - return fetchList( - sort: currentState.sort, - startLevel: currentState.startLevel, - endLevel: currentState.endLevel - ) - - case .fetchListFilter: - return fetchList( - sort: currentState.sort, - startLevel: currentState.startLevel ?? 1, - endLevel: currentState.endLevel ?? 200 - ) - + return fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel) case .undoLastDeletedBookmark: return handleUndoLastDeletedBookmark() - + case let .toggleBookmark(id, isSelected): + return handleToggleBookmark(id: id, isSelected: isSelected) case let .itemFilterOptionSelected(results): return handleItemFilterOptionSelected(results: results) } @@ -166,9 +149,7 @@ open class DictionaryListReactor: Reactor { var copy = item copy.bookmarkId = updated.bookmarkId ?? item.bookmarkId return copy - } else { - return item - } + } else { return item } } } else { if newState.currentPage == 0 { @@ -198,10 +179,12 @@ open class DictionaryListReactor: Reactor { newState.categoryIds = id case let .updateBookmarkState(id, isSelected): if let index = newState.listItems.firstIndex(where: { $0.id == id }) { - if isSelected { - newState.listItems[index].bookmarkId = newState.listItems[index].bookmarkId ?? -1 - } else { - newState.listItems[index].bookmarkId = nil + newState.listItems[index].bookmarkId = isSelected ? (newState.listItems[index].bookmarkId ?? -1) : nil + } + case let .updateBookmarkStates(dict): + for index in 0.. Observable { + + func fetchList(sort: String?, startLevel: Int?, endLevel: Int?, updateBookmarkOnly: Bool = false) -> Observable { let response: Observable switch currentState.type { @@ -232,7 +211,6 @@ private extension DictionaryListReactor { maxLevel: endLevel ) ) - case .item: response = dictionaryItemListUseCase.execute( keyword: currentState.keyword ?? "", @@ -244,7 +222,6 @@ private extension DictionaryListReactor { size: 20, sort: sort ) - case .map: response = dictionaryMapListUseCase.execute( keyword: currentState.keyword ?? "", @@ -252,7 +229,6 @@ private extension DictionaryListReactor { size: 20, sort: sort ?? "ASC" ) - case .npc: response = dictionaryNpcListUseCase.execute( keyword: currentState.keyword ?? "", @@ -260,7 +236,6 @@ private extension DictionaryListReactor { size: 20, sort: sort ?? "ASC" ) - case .quest: response = dictionaryQuestListUseCase.execute( keyword: currentState.keyword ?? "", @@ -268,32 +243,26 @@ private extension DictionaryListReactor { size: 20, sort: sort ?? "ASC" ) - case .total: response = dictionaryAllListUseCase.execute( keyword: currentState.keyword ?? "", page: currentState.currentPage ) - default: return .empty() } return response.map { response in - var mergedItems: [DictionaryMainItemResponse] - if updateBookmarkOnly { - mergedItems = self.currentState.listItems.map { item in + let merged = self.currentState.listItems.map { item in if let updated = response.contents.first(where: { $0.id == item.id }) { var copy = item copy.bookmarkId = updated.bookmarkId ?? item.bookmarkId return copy - } else { - return item - } + } else { return item } } var newResponse = response - newResponse.contents = mergedItems + newResponse.contents = merged return .setListItem(newResponse, updateBookmarkOnly: true) } else { return .setListItem(response) @@ -304,30 +273,30 @@ private extension DictionaryListReactor { func handleViewWillAppear() -> Observable { let loginState = checkLoginUseCase.execute() .map { Mutation.setLoginState($0) } - - let fetchMutation = fetchList( - sort: currentState.sort, - startLevel: currentState.startLevel, - endLevel: currentState.endLevel - ) - + let fetchMutation = fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel) return .merge([loginState, fetchMutation]) } - private func handleToggleBookmark(id: Int, isSelected: Bool) -> Observable { + func handleToggleBookmark(id: Int, isSelected: Bool) -> Observable { guard let index = currentState.listItems.firstIndex(where: { $0.id == id }) else { return .empty() } let targetItem = currentState.listItems[index] - let saveDeleted = isSelected - ? Observable.just(Mutation.setLastDeletedBookmark(targetItem)) - : .empty() - - let optimistic = Observable.just(Mutation.updateBookmarkState( - id: id, - isSelected: !isSelected - )) + let optimistic: Observable + + if isSelected { + // 삭제되는 경우, undo를 위해 lastDeletedBookmark 저장 + optimistic = Observable.concat([ + // UI 반영 + .just(.updateBookmarkState(id: id, isSelected: false)), + // undo 저장 + .just(.setLastDeletedBookmark(targetItem)) + ]) + } else { + // 북마크 추가 + optimistic = .just(.updateBookmarkState(id: id, isSelected: true)) + } - // 서버 호출 후 bookmarkId 확정 + // 서버 호출 + bookmark 확정 let api = setBookmarkUseCase.execute( bookmarkId: isSelected ? targetItem.bookmarkId ?? targetItem.id : targetItem.id, isBookmark: isSelected ? .delete : .set(targetItem.type) @@ -338,8 +307,9 @@ private extension DictionaryListReactor { endLevel: currentState.endLevel, updateBookmarkOnly: true )) + .observe(on: MainScheduler.asyncInstance) - return .concat([saveDeleted, optimistic, api]) + return .concat([optimistic, api]) } func handleSortOptionSelected(sort: SortType) -> Observable { @@ -347,6 +317,10 @@ private extension DictionaryListReactor { .just(.setSort(sort.sortParameter)), .just(.initPage) ]) + .concat(Observable.deferred { [weak self] in + guard let self = self else { return .empty() } + return self.fetchList(sort: self.currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel) + }) } func handleFilterOptionSelected(startLevel: Int?, endLevel: Int?) -> Observable { @@ -356,11 +330,7 @@ private extension DictionaryListReactor { ]) .concat(Observable.deferred { [weak self] in guard let self = self else { return .empty() } - return self.fetchList( - sort: self.currentState.sort, - startLevel: startLevel, - endLevel: endLevel - ) + return self.fetchList(sort: self.currentState.sort, startLevel: startLevel, endLevel: endLevel) }) } @@ -368,20 +338,16 @@ private extension DictionaryListReactor { guard let lastDeleted = currentState.lastDeletedBookmark else { return .empty() } let optimistic = Observable.just(Mutation.updateBookmarkState(id: lastDeleted.id, isSelected: true)) + .observe(on: MainScheduler.asyncInstance) - let api = setBookmarkUseCase.execute( + let apiCall = setBookmarkUseCase.execute( bookmarkId: lastDeleted.id, isBookmark: .set(lastDeleted.type) ) - .andThen(fetchList( - sort: currentState.sort, - startLevel: currentState.startLevel, - endLevel: currentState.endLevel, - updateBookmarkOnly: true - )) - .concat(Observable.just(Mutation.setLastDeletedBookmark(nil))) + .andThen(Observable.just(Mutation.setLastDeletedBookmark(nil))) + .observe(on: MainScheduler.asyncInstance) - return .concat([optimistic, api]) + return .concat([optimistic, apiCall]) } func handleItemFilterOptionSelected(results: [(String, String)]) -> Observable { @@ -394,11 +360,7 @@ private extension DictionaryListReactor { ]) .concat(Observable.deferred { [weak self] in guard let self = self else { return .empty() } - return self.fetchList( - sort: self.currentState.sort, - startLevel: self.currentState.startLevel, - endLevel: self.currentState.endLevel - ) + return self.fetchList(sort: self.currentState.sort, startLevel: self.currentState.startLevel, endLevel: self.currentState.endLevel) }) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift index b0e5392f..8c2f15ec 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift @@ -125,28 +125,6 @@ extension DictionaryListViewController { .bind(to: itemCountRelay) .disposed(by: disposeBag) - reactor.state.map(\.listItems) - .observe(on: MainScheduler.instance) - .bind(onNext: { [weak self] items in - guard let self = self else { return } - self.mainView.emptyView.isHidden = !items.isEmpty - self.mainView.listCollectionView.isHidden = items.isEmpty - - if self.reactor?.currentState.currentPage == 0 || self.reactor?.currentState.isBookmarkUpdateOnly != true { - self.mainView.listCollectionView.reloadData() - } else { - let collectionView = self.mainView.listCollectionView - for cell in collectionView.visibleCells { - if let indexPath = collectionView.indexPath(for: cell), - indexPath.item < items.count, - let cell = cell as? DictionaryListCell { - cell.updateBookmarkState(isBookmarked: items[indexPath.item].bookmarkId != nil) - } - } - } - }) - .disposed(by: disposeBag) - rx.viewWillAppear .map { _ in Reactor.Action.viewWillAppear } .bind(to: reactor.action) @@ -167,7 +145,6 @@ extension DictionaryListViewController { let selectedFilter = reactor.currentState.type.bookmarkSortedFilter[index] reactor.action.onNext(.sortOptionSelected(selectedFilter)) owner.mainView.selectSort(selectedType: selectedFilter) - reactor.action.onNext(.fetchListFilter) } owner.tabBarController?.presentModal(viewController) case .filter(let type): @@ -210,9 +187,42 @@ extension DictionaryListViewController { bookmarkChangeRelay .observe(on: MainScheduler.instance) .bind(onNext: { [weak self] id, isBookmarked in - self?.reactor?.action.onNext(.toggleBookmark(id, isBookmarked)) + self?.reactor?.action.onNext(.toggleBookmark(id: id, isSelected: isBookmarked)) }) .disposed(by: disposeBag) + + // 기존 bookmarkChangeRelay 사용 대신 + reactor.state.map(\.listItems) + .observe(on: MainScheduler.instance) + .bind { [weak self] items in + guard let self = self else { return } + let collectionView = self.mainView.listCollectionView + self.mainView.emptyView.isHidden = !items.isEmpty + self.mainView.listCollectionView.isHidden = items.isEmpty + + if self.reactor?.currentState.currentPage == 0, !self.reactor!.currentState.isBookmarkUpdateOnly { + collectionView.reloadData() + } else { + let startIndex = collectionView.numberOfItems(inSection: 0) + let endIndex = items.count + if endIndex > startIndex { + let indexPaths = (startIndex ..< endIndex).map { IndexPath(item: $0, section: 0) } + collectionView.performBatchUpdates { + collectionView.insertItems(at: indexPaths) + } + } + + for cell in collectionView.visibleCells { + if let indexPath = collectionView.indexPath(for: cell), + indexPath.item < items.count, + let cell = cell as? DictionaryListCell { + let item = items[indexPath.item] + cell.updateBookmarkState(isBookmarked: item.bookmarkId != nil) + } + } + } + } + .disposed(by: disposeBag) } } @@ -258,7 +268,7 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi return } - self.reactor?.action.onNext(.toggleBookmark(item.id, isSelected)) + self.reactor?.action.onNext(.toggleBookmark(id: item.id, isSelected: isSelected)) if isSelected { SnackBarFactory.createSnackBar( @@ -279,25 +289,29 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi text: "아이템을 북마크에 추가했어요.", buttonText: "컬렉션 추가", buttonAction: { - DispatchQueue.main.async { [weak self] in - guard let self = self, - let id = item.bookmarkId else { return } - - let vc = self.bookmarkModalFactory.make(bookmarkIds: [id]) { isAdd in - if isAdd { - ToastFactory.createToast( - message: "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요." - ) - } + self.reactor?.state.map(\.listItems) + .compactMap { list in + list.first(where: { $0.id == item.id })?.bookmarkId } - vc.modalPresentationStyle = .pageSheet - if let sheet = vc.sheetPresentationController { - sheet.detents = [.medium(), .large()] - sheet.prefersGrabberVisible = true - sheet.preferredCornerRadius = 16 - } - self.present(vc, animated: true) - } + .take(1) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { bookmarkId in + let vc = self.bookmarkModalFactory.make(bookmarkIds: [bookmarkId]) { isAdd in + if isAdd { + ToastFactory.createToast( + message: "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요." + ) + } + } + vc.modalPresentationStyle = .pageSheet + if let sheet = vc.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + sheet.preferredCornerRadius = 16 + } + self.present(vc, animated: true) + }) + .disposed(by: self.disposeBag) } ) } @@ -307,9 +321,7 @@ extension DictionaryListViewController: UICollectionViewDelegate, UICollectionVi return cell } - public func collectionView( - _ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath - ) { + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let reactor = reactor else { return } let item: DictionaryMainItemResponse From 161ec3f8ac53f177e340c1cbbc3cd2064b9e8d04 Mon Sep 17 00:00:00 2001 From: p2glet Date: Tue, 9 Dec 2025 17:36:12 +0900 Subject: [PATCH 09/34] =?UTF-8?q?fix/#273:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=97=AC=EB=B6=80=20/=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=9C=A0=EB=AC=B4=20=EC=97=AC=EB=B6=80=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20UI=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthAPI/CheckLoginUseCaseImpl.swift | 27 ++++- MLS/MLS/Application/AppDelegate.swift | 7 +- .../BaseFeature/Shared/BaseListView.swift | 17 +-- .../BaseFeature/Shared/BookmarkStore.swift | 28 ----- .../BaseFeature/Shared/DataEmptyView.swift | 104 ++++++++++++++++++ .../BaseFeature/Shared/ToLoginView.swift} | 22 ++-- .../BookmarkList/BookmarkListView.swift | 26 +---- .../BookmarkListViewController.swift | 15 +-- .../BookmarkMainFactoryImpl.swift | 13 ++- .../BookmarkMain/BookmarkMainReactor.swift | 22 ++-- .../BookmarkMain/BookmarkMainView.swift | 58 +++++----- .../BookmarkMainViewController.swift | 34 ++++-- .../DictionaryListEmptyView.swift | 60 ---------- .../DictionaryList/DictionaryListView.swift | 26 ++--- .../DictionaryListViewController.swift | 13 ++- 15 files changed, 254 insertions(+), 218 deletions(-) delete mode 100644 MLS/Presentation/BaseFeature/BaseFeature/Shared/BookmarkStore.swift create mode 100644 MLS/Presentation/BaseFeature/BaseFeature/Shared/DataEmptyView.swift rename MLS/Presentation/{BookmarkFeature/BookmarkFeature/BookmarkEmpty/BookmarkEmptyView.swift => BaseFeature/BaseFeature/Shared/ToLoginView.swift} (76%) delete mode 100644 MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListEmptyView.swift diff --git a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift index c3cd42df..1993ad45 100644 --- a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift @@ -1,10 +1,22 @@ import DomainInterface +import RxRelay import RxSwift -public class CheckLoginUseCaseImpl: CheckLoginUseCase { +public final class CheckLoginUseCaseImpl: CheckLoginUseCase { private let authRepository: AuthAPIRepository private let tokenRepository: TokenRepository + private let disposeBag = DisposeBag() + + private let loginCheckRelay = PublishRelay() + private lazy var sharedLoginCheck: Observable = { + loginCheckRelay + .flatMapLatest { [weak self] _ -> Observable in + guard let self else { return .just(false) } + return self.executeInternal() + } + .share(replay: 1, scope: .forever) + }() public init(authRepository: AuthAPIRepository, tokenRepository: TokenRepository) { self.authRepository = authRepository @@ -12,8 +24,18 @@ public class CheckLoginUseCaseImpl: CheckLoginUseCase { } public func execute() -> Observable { + return Observable.deferred { [weak self] in + guard let self else { return .just(false) } + self.loginCheckRelay.accept(()) + return self.sharedLoginCheck + } + } + + private func executeInternal() -> Observable { switch tokenRepository.fetchToken(type: .refreshToken) { case .success(let token): + guard !token.isEmpty else { return .just(false) } + return authRepository.reissueToken(refreshToken: token) .map { [weak self] response in guard let self else { return false } @@ -35,8 +57,7 @@ public class CheckLoginUseCaseImpl: CheckLoginUseCase { return .just(false) } - case .failure(let error): - print("refreshToken 불러오기 실패:", error.localizedDescription) + case .failure: return .just(false) } } diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index 01d3a82b..f01266b8 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -996,9 +996,10 @@ extension AppDelegate { } DIContainer.register(type: BookmarkMainFactory.self) { BookmarkMainFactoryImpl( - setBookmarkUseCase: - DIContainer + setBookmarkUseCase: DIContainer .resolve(type: SetBookmarkUseCase.self), + checkLoginUseCase: DIContainer + .resolve(type: CheckLoginUseCase.self), onBoardingFactory: DIContainer .resolve(type: BookmarkOnBoardingFactory.self), @@ -1013,7 +1014,7 @@ extension AppDelegate { .resolve(type: DictionarySearchFactory.self), notificationFactory: DIContainer.resolve( type: DictionaryNotificationFactory.self - ) + ), loginFactory: DIContainer.resolve(type: LoginFactory.self) ) } DIContainer.register(type: BookmarkOnBoardingFactory.self) { diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Shared/BaseListView.swift b/MLS/Presentation/BaseFeature/BaseFeature/Shared/BaseListView.swift index 033df7e2..bfad802d 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/Shared/BaseListView.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/Shared/BaseListView.swift @@ -25,7 +25,7 @@ open class BaseListView: UIView { public let listCollectionView: UICollectionView public let sortButton: UIButton public let filterButton: UIButton - public let emptyView: UIView + public let emptyView: DataEmptyView private lazy var filterStackView: UIStackView = { var subviews: [UIView] = [] @@ -48,7 +48,7 @@ open class BaseListView: UIView { public init(editButton: UIButton? = nil, sortButton: UIButton, filterButton: UIButton, - emptyView: UIView, + emptyView: DataEmptyView, isFilterHidden: Bool) { self.editButton = editButton self.sortButton = sortButton @@ -81,6 +81,10 @@ private extension BaseListView { listCollectionView.snp.makeConstraints { make in make.edges.equalToSuperview() } + + emptyView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } } else { filterStackView.snp.makeConstraints { make in make.top.equalToSuperview().inset(Constant.topMargin) @@ -92,11 +96,11 @@ private extension BaseListView { make.top.equalTo(filterStackView.snp.bottom).offset(Constant.topMargin) make.horizontalEdges.bottom.equalToSuperview() } - } - emptyView.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.centerY.equalToSuperview().offset(-Constant.bottomInset) + emptyView.snp.makeConstraints { make in + make.top.equalTo(filterStackView.snp.bottom).offset(Constant.topMargin) + make.horizontalEdges.bottom.equalToSuperview() + } } } @@ -169,7 +173,6 @@ public extension BaseListView { func checkEmptyData(isEmpty: Bool) { emptyView.isHidden = !isEmpty - filterStackView.isHidden = isEmpty listCollectionView.isHidden = isEmpty } } diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Shared/BookmarkStore.swift b/MLS/Presentation/BaseFeature/BaseFeature/Shared/BookmarkStore.swift deleted file mode 100644 index af9543d4..00000000 --- a/MLS/Presentation/BaseFeature/BaseFeature/Shared/BookmarkStore.swift +++ /dev/null @@ -1,28 +0,0 @@ -//import RxSwift -//import RxCocoa -// -//public final class BookmarkStore { -// public static let shared = BookmarkStore() -// -// private let disposeBag = DisposeBag() -// -// // BehaviorRelay 사용 -// private let bookmarksRelay = BehaviorRelay<[Int: Bool]>(value: [:]) -// -// // 외부에서 Observable로 구독 가능 -// public var bookmarks: Observable<[Int: Bool]> { -// return bookmarksRelay.asObservable() -// } -// -// private init() {} -// -// public func setBookmark(id: Int, isBookmarked: Bool) { -// var current = bookmarksRelay.value -// current[id] = isBookmarked -// bookmarksRelay.accept(current) -// } -// -// public func isBookmarked(id: Int) -> Bool { -// return bookmarksRelay.value[id] ?? false -// } -//} diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Shared/DataEmptyView.swift b/MLS/Presentation/BaseFeature/BaseFeature/Shared/DataEmptyView.swift new file mode 100644 index 00000000..f92b8c2b --- /dev/null +++ b/MLS/Presentation/BaseFeature/BaseFeature/Shared/DataEmptyView.swift @@ -0,0 +1,104 @@ +import UIKit + +import DesignSystem + +import SnapKit + +public enum EmptyViewType { + case dictionary + case bookmark +} + +public final class DataEmptyView: UIView { + // MARK: - Type + enum Constant { + static let imageSize: CGFloat = 220 + static let textSpacing: CGFloat = 10 + static let buttonSpacing: CGFloat = 24 + static let buttonWidth: CGFloat = 186 + } + + // MARK: - Components + public let imageView = UIImageView() + private let mainLabel = UILabel() + private let subLabel = UILabel() + + public let button = CommonButton() + + // MARK: - Init + public init(type: EmptyViewType) { + super.init(frame: .zero) + addViews() + setupConstraints() + configureUI(type: type) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - SetUp +private extension DataEmptyView { + func addViews() { + addSubview(imageView) + addSubview(mainLabel) + addSubview(subLabel) + addSubview(button) + } + + func setupConstraints() { + imageView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.size.equalTo(Constant.imageSize) + } + + mainLabel.snp.makeConstraints { make in + make.top.equalTo(imageView.snp.bottom) + make.centerY.equalToSuperview() + make.horizontalEdges.equalToSuperview() + } + + subLabel.snp.makeConstraints { make in + make.top.equalTo(mainLabel.snp.bottom).offset(Constant.textSpacing) + make.centerX.equalToSuperview() + } + + button.snp.makeConstraints { make in + make.top.equalTo(subLabel.snp.bottom).offset(Constant.buttonSpacing) + make.centerX.equalToSuperview() + make.width.equalTo(Constant.buttonWidth) + } + } + + func configureUI(type: EmptyViewType) { + backgroundColor = .neutral100 + + switch type { + case .dictionary: + imageView.image = DesignSystemAsset.image(named: "noResult") + mainLabel.attributedText = .makeStyledString( + font: .b_m_r, + text: "검색 결과가 없습니다." + ) + + subLabel.isHidden = true + button.isHidden = true + case .bookmark: + imageView.image = DesignSystemAsset.image(named: "noShowList") + mainLabel.attributedText = .makeStyledString( + font: .h_xl_b, + text: "아직 아무것도 없어요!" + ) + + subLabel.attributedText = .makeStyledString( + font: .cp_s_r, + text: "북마크해서 추가해보세요.", + color: .neutral600 + ) + + button.updateTitle(title: "북마크하러 가기") + } + } +} diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkEmpty/BookmarkEmptyView.swift b/MLS/Presentation/BaseFeature/BaseFeature/Shared/ToLoginView.swift similarity index 76% rename from MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkEmpty/BookmarkEmptyView.swift rename to MLS/Presentation/BaseFeature/BaseFeature/Shared/ToLoginView.swift index 2103c5b6..5be73445 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkEmpty/BookmarkEmptyView.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/Shared/ToLoginView.swift @@ -4,7 +4,7 @@ import DesignSystem import SnapKit -final class BookmarkEmptyView: UIView { +public final class ToLoginView: UIView { // MARK: - Type enum Constant { static let imageSize: CGFloat = 220 @@ -21,7 +21,7 @@ final class BookmarkEmptyView: UIView { public let button = CommonButton() // MARK: - Init - init() { + public init() { super.init(frame: .zero) addViews() setupConstraints() @@ -35,7 +35,7 @@ final class BookmarkEmptyView: UIView { } // MARK: - SetUp -private extension BookmarkEmptyView { +private extension ToLoginView { func addViews() { addSubview(imageView) addSubview(mainLabel) @@ -45,13 +45,13 @@ private extension BookmarkEmptyView { func setupConstraints() { imageView.snp.makeConstraints { make in - make.top.equalToSuperview() make.centerX.equalToSuperview() make.size.equalTo(Constant.imageSize) } mainLabel.snp.makeConstraints { make in make.top.equalTo(imageView.snp.bottom) + make.centerY.equalToSuperview() make.horizontalEdges.equalToSuperview() } @@ -62,29 +62,25 @@ private extension BookmarkEmptyView { button.snp.makeConstraints { make in make.top.equalTo(subLabel.snp.bottom).offset(Constant.buttonSpacing) - make.centerX.bottom.equalToSuperview() + make.centerX.equalToSuperview() make.width.equalTo(Constant.buttonWidth) } } func configureUI() { backgroundColor = .neutral100 - } -} - -extension BookmarkEmptyView { - func setLabel(isLogin: Bool) { imageView.image = DesignSystemAsset.image(named: "noShowList") mainLabel.attributedText = .makeStyledString( font: .h_xl_b, - text: isLogin ? "아직 아무것도 없어요!" : "북마크는 로그인 후 이용 가능해요!" + text: "북마크는 로그인 후 이용 가능해요!" ) subLabel.attributedText = .makeStyledString( font: .cp_s_r, - text: isLogin ? "북마크해서 추가해보세요." : "자주 보는 정보, 검색 없이 바로 확인 할 수 있어요." + text: "자주 보는 정보, 검색 없이 바로 확인 할 수 있어요.", + color: .neutral600 ) - button.updateTitle(title: isLogin ? "북마크하러 가기" : "로그인하러 가기") + button.updateTitle(title: "로그인하러 가기") } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListView.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListView.swift index e70894a3..fe335d9a 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListView.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListView.swift @@ -4,10 +4,10 @@ import BaseFeature import DesignSystem final class BookmarkListView: BaseListView { - let bookmarkEmptyView: BookmarkEmptyView + let bookmarkEmptyView: DataEmptyView // MARK: - Init - init(isFilterHidden: Bool, bookmarkEmptyView: BookmarkEmptyView) { + init(isFilterHidden: Bool, bookmarkEmptyView: DataEmptyView) { let editButton = TextButton() let sortButton = BaseListView.makeSortButton(title: "가나다 순", tintColor: .textColor) let filterButton = BaseListView.makeFilterButton(title: "필터", tintColor: .textColor) @@ -24,25 +24,3 @@ final class BookmarkListView: BaseListView { @available(*, unavailable) required init?(coder: NSCoder) { fatalError() } } - -extension BookmarkListView { - func updateView(state: BookmarkListReactor.ViewState) { - switch state { - case .loginWithData: - checkEmptyData(isEmpty: false) - - case .loginWithoutData: - checkEmptyData(isEmpty: true) - if let emptyView = emptyView as? BookmarkEmptyView { - checkEmptyData(isEmpty: true) - emptyView.setLabel(isLogin: true) - } - - case .logout: - if let emptyView = emptyView as? BookmarkEmptyView { - checkEmptyData(isEmpty: true) - emptyView.setLabel(isLogin: false) - } - } - } -} diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift index 997458a2..d8316456 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift @@ -28,7 +28,7 @@ public final class BookmarkListViewController: BaseViewController, View { // MARK: - Components private var mainView: BookmarkListView - private var emptyView = BookmarkEmptyView() + private var emptyView = DataEmptyView(type: .bookmark) public init( reactor: BookmarkListReactor, @@ -137,7 +137,8 @@ extension BookmarkListViewController { .distinctUntilChanged() .withUnretained(self) .observe(on: MainScheduler.instance) - .bind(onNext: { owner, _ in + .bind(onNext: { owner, items in + owner.mainView.checkEmptyData(isEmpty: items.isEmpty) owner.mainView.listCollectionView.reloadData() }) .disposed(by: disposeBag) @@ -207,16 +208,6 @@ extension BookmarkListViewController { owner.mainView.updateFilter(sortType: type.bookmarkSortedFilter.first) }) .disposed(by: disposeBag) - - reactor.state - .map(\.viewState) - .distinctUntilChanged() - .withUnretained(self) - .observe(on: MainScheduler.instance) - .bind(onNext: { owner, state in - owner.mainView.updateView(state: state) - }) - .disposed(by: disposeBag) } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift index 4e1ec637..76e5ed3e 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift @@ -1,3 +1,4 @@ +import AuthFeatureInterface import BaseFeature import BookmarkFeatureInterface import DictionaryFeatureInterface @@ -5,31 +6,38 @@ import DomainInterface public final class BookmarkMainFactoryImpl: BookmarkMainFactory { private let setBookmarkUseCase: SetBookmarkUseCase + private let checkLoginUseCase: CheckLoginUseCase + private let onBoardingFactory: BookmarkOnBoardingFactory private let bookmarkListFactory: BookmarkListFactory private let collectionListFactory: CollectionListFactory private let searchFactory: DictionarySearchFactory private let notificationFactory: DictionaryNotificationFactory + private let loginFactory: LoginFactory public init( setBookmarkUseCase: SetBookmarkUseCase, + checkLoginUseCase: CheckLoginUseCase, onBoardingFactory: BookmarkOnBoardingFactory, bookmarkListFactory: BookmarkListFactory, collectionListFactory: CollectionListFactory, searchFactory: DictionarySearchFactory, - notificationFactory: DictionaryNotificationFactory + notificationFactory: DictionaryNotificationFactory, + loginFactory: LoginFactory ) { self.setBookmarkUseCase = setBookmarkUseCase + self.checkLoginUseCase = checkLoginUseCase self.onBoardingFactory = onBoardingFactory self.bookmarkListFactory = bookmarkListFactory self.collectionListFactory = collectionListFactory self.searchFactory = searchFactory self.notificationFactory = notificationFactory + self.loginFactory = loginFactory } public func make() -> BaseViewController { let reactor = BookmarkMainReactor( - setBookmarkUseCase: setBookmarkUseCase + setBookmarkUseCase: setBookmarkUseCase, checkLoginUseCase: checkLoginUseCase ) let viewController = BookmarkMainViewController( onBoardingFactory: onBoardingFactory, @@ -37,6 +45,7 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { collectionListFactory: collectionListFactory, searchFactory: searchFactory, notificationFactory: notificationFactory, + loginFactory: loginFactory, reactor: reactor ) return viewController diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift index 7526273c..5b43f8b5 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift @@ -9,17 +9,19 @@ public final class BookmarkMainReactor: Reactor { case onBoarding case notification case edit + case login } public enum Action { - case viewDidAppear - case dismissOnboarding + case viewWillAppear case searchButtonTapped case notificationButtonTapped + case loginButtonTapped } public enum Mutation { case navigateTo(Route) + case setLogin(Bool) } public struct State { @@ -28,30 +30,34 @@ public final class BookmarkMainReactor: Reactor { var sections: [String] { return type.pageTabList.map { $0.title } } + var isLogin = false } // MARK: - Properties private let setBookmarkUseCase: SetBookmarkUseCase + private let checkLoginUseCase: CheckLoginUseCase public var initialState: State private let disposeBag = DisposeBag() - public init(setBookmarkUseCase: SetBookmarkUseCase) { + public init(setBookmarkUseCase: SetBookmarkUseCase, checkLoginUseCase: CheckLoginUseCase) { self.initialState = State(route: .none) self.setBookmarkUseCase = setBookmarkUseCase + self.checkLoginUseCase = checkLoginUseCase } public func mutate(action: Action) -> Observable { switch action { - case .viewDidAppear: - return .empty() - case .dismissOnboarding: - return .empty() + case .viewWillAppear: + return checkLoginUseCase.execute() + .map { .setLogin($0) } case .searchButtonTapped: return Observable.just(.navigateTo(.search)) case .notificationButtonTapped: return Observable.just(.navigateTo(.notification)) + case .loginButtonTapped: + return .just(.navigateTo(.login)) } } @@ -60,6 +66,8 @@ public final class BookmarkMainReactor: Reactor { switch mutation { case .navigateTo(let route): newState.route = route + case let .setLogin(isLogin): + newState.isLogin = isLogin } return newState } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift index e2709211..e2fe8853 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift @@ -1,9 +1,8 @@ -import UIKit - +import BaseFeature import DesignSystem import DomainInterface - import SnapKit +import UIKit final class BookmarkMainView: UIView { enum Constant { @@ -14,7 +13,6 @@ final class BookmarkMainView: UIView { // MARK: - Components public let headerView = Header(style: .main, title: "북마크") - public let searchBar = SearchBar() public let tabCollectionView: UICollectionView = { @@ -29,11 +27,12 @@ final class BookmarkMainView: UIView { navigationOrientation: .horizontal ) + public let emptyView = ToLoginView() + // MARK: - Init public init(type: DictionaryMainViewType) { super.init(frame: .zero) - addViews(type: type) - setupConstraints(type: type) + setupBaseLayout(type: type) } @available(*, unavailable) @@ -42,43 +41,35 @@ final class BookmarkMainView: UIView { } } -// MARK: - SetUp +// MARK: - Base Layout private extension BookmarkMainView { - func addViews(type: DictionaryMainViewType) { + func setupBaseLayout(type: DictionaryMainViewType) { switch type { case .search: addSubview(searchBar) - default: - addSubview(headerView) - } - addSubview(tabCollectionView) - addSubview(pageViewController.view) - } - - func setupConstraints(type: DictionaryMainViewType) { - switch type { - case .search: searchBar.snp.makeConstraints { make in make.top.equalTo(safeAreaLayoutGuide) make.horizontalEdges.equalToSuperview() } - - tabCollectionView.snp.makeConstraints { make in - make.top.equalTo(searchBar.snp.bottom).offset(Constant.topMargin) - make.horizontalEdges.equalToSuperview() - make.height.equalTo(Constant.pageTabHeight) - } - - pageViewController.view.snp.makeConstraints { make in - make.top.equalTo(tabCollectionView.snp.bottom) - make.horizontalEdges.equalTo(safeAreaLayoutGuide) - make.bottom.equalToSuperview() - } default: + addSubview(headerView) headerView.snp.makeConstraints { make in make.top.equalTo(safeAreaLayoutGuide) make.horizontalEdges.equalToSuperview() } + } + } +} + +// MARK: - Public Update +extension BookmarkMainView { + public func updateLoginState(isLogin: Bool) { + // 기존 서브뷰 제거 + [tabCollectionView, pageViewController.view, emptyView].forEach { $0.removeFromSuperview() } + + if isLogin { + addSubview(tabCollectionView) + addSubview(pageViewController.view) tabCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(Constant.topMargin) @@ -91,6 +82,13 @@ private extension BookmarkMainView { make.horizontalEdges.equalTo(safeAreaLayoutGuide) make.bottom.equalToSuperview().inset(Constant.bottomTabHeight) } + } else { + addSubview(emptyView) + emptyView.snp.makeConstraints { make in + make.top.equalTo(headerView.snp.bottom).offset(Constant.topMargin) + make.horizontalEdges.equalToSuperview() + make.bottom.equalToSuperview().inset(Constant.bottomTabHeight) + } } } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift index 1352a597..efca0833 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift @@ -1,5 +1,6 @@ import UIKit +import AuthFeatureInterface import BaseFeature import BookmarkFeatureInterface import DictionaryFeatureInterface @@ -21,6 +22,7 @@ public final class BookmarkMainViewController: BaseViewController, View { private var onBoardingFactory: BookmarkOnBoardingFactory private let searchFactory: DictionarySearchFactory private let notificationFactory: DictionaryNotificationFactory + private let loginFactory: LoginFactory private var viewControllers: [UIViewController] @@ -34,6 +36,7 @@ public final class BookmarkMainViewController: BaseViewController, View { collectionListFactory: CollectionListFactory, searchFactory: DictionarySearchFactory, notificationFactory: DictionaryNotificationFactory, + loginFactory: LoginFactory, reactor: BookmarkMainReactor ) { let type = reactor.currentState.type @@ -48,6 +51,7 @@ public final class BookmarkMainViewController: BaseViewController, View { self.onBoardingFactory = onBoardingFactory self.searchFactory = searchFactory self.notificationFactory = notificationFactory + self.loginFactory = loginFactory self.initialIndex = initialIndex super.init() self.reactor = reactor @@ -130,8 +134,8 @@ public extension BookmarkMainViewController { } func bindUserActions(reactor: Reactor) { - rx.viewDidAppear - .map { _ in Reactor.Action.viewDidAppear } + rx.viewWillAppear + .map { _ in Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -144,6 +148,11 @@ public extension BookmarkMainViewController { .map { Reactor.Action.notificationButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) + + mainView.emptyView.button.rx.tap + .map { Reactor.Action.loginButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) } func bindViewState(reactor: Reactor) { @@ -162,20 +171,25 @@ public extension BookmarkMainViewController { case .onBoarding: let viewController = owner.onBoardingFactory.make() viewController.modalPresentationStyle = .fullScreen - - viewController.rx.deallocated - .take(1) - .subscribe(onNext: { - reactor.action.onNext(.dismissOnboarding) - }) - .disposed(by: owner.disposeBag) - owner.present(viewController, animated: true) + case .login: + let controller = owner.loginFactory.make(exitRoute: .pop, onLoginCompleted: nil) + owner.navigationController?.pushViewController(controller, animated: true) default: break } } .disposed(by: disposeBag) + + reactor.state + .map { $0.isLogin } + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind { owner, isLogin in + owner.mainView.updateLoginState(isLogin: isLogin) + } + .disposed(by: disposeBag) + } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListEmptyView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListEmptyView.swift deleted file mode 100644 index 0ddde3c6..00000000 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListEmptyView.swift +++ /dev/null @@ -1,60 +0,0 @@ -import UIKit - -import DesignSystem - -import SnapKit - -final class DictionaryListEmptyView: UIView { - // MARK: - Type - enum Constant { - static let imageSize: CGFloat = 220 - static let topInset: CGFloat = 12 - static let spacing: CGFloat = 4 - } - - // MARK: - Components - public let imageView: UIImageView = { - let view = UIImageView() - view.image = DesignSystemAsset.image(named: "noResult") - return view - }() - - private let textLabel: UILabel = { - let label = UILabel() - label.attributedText = .makeStyledString(font: .b_m_r, text: "검색 결과가 없습니다.") - return label - }() - - // MARK: - Init - init() { - super.init(frame: .zero) - addViews() - setupConstraints() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SetUp -private extension DictionaryListEmptyView { - func addViews() { - addSubview(imageView) - addSubview(textLabel) - } - - func setupConstraints() { - imageView.snp.makeConstraints { make in - make.top.equalToSuperview().inset(Constant.topInset) - make.centerX.equalToSuperview() - make.size.equalTo(Constant.imageSize) - } - - textLabel.snp.makeConstraints { make in - make.top.equalTo(imageView.snp.bottom).offset(Constant.spacing) - make.centerX.bottom.equalToSuperview() - } - } -} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListView.swift index ddee29e4..d32a7174 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListView.swift @@ -6,19 +6,19 @@ import DesignSystem final class DictionaryListView: BaseListView { // MARK: - Init init(isFilterHidden: Bool) { - let sortButton = BaseListView.makeSortButton(title: "가나다 순", tintColor: .neutral900) - let filterButton = BaseListView.makeFilterButton(title: "필터", tintColor: .neutral900) - let emptyView = DictionaryListEmptyView() + let sortButton = BaseListView.makeSortButton(title: "가나다 순", tintColor: .neutral900) + let filterButton = BaseListView.makeFilterButton(title: "필터", tintColor: .neutral900) + let emptyView = DataEmptyView(type: .dictionary) - super.init( - editButton: nil, - sortButton: sortButton, - filterButton: filterButton, - emptyView: emptyView, - isFilterHidden: isFilterHidden - ) - } + super.init( + editButton: nil, + sortButton: sortButton, + filterButton: filterButton, + emptyView: emptyView, + isFilterHidden: isFilterHidden + ) + } - @available(*, unavailable) - required init?(coder: NSCoder) { fatalError() } + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift index 8c2f15ec..0fed74a2 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift @@ -194,13 +194,14 @@ extension DictionaryListViewController { // 기존 bookmarkChangeRelay 사용 대신 reactor.state.map(\.listItems) .observe(on: MainScheduler.instance) - .bind { [weak self] items in - guard let self = self else { return } - let collectionView = self.mainView.listCollectionView - self.mainView.emptyView.isHidden = !items.isEmpty - self.mainView.listCollectionView.isHidden = items.isEmpty + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind { owner, items in + let collectionView = owner.mainView.listCollectionView + owner.mainView.checkEmptyData(isEmpty: items.isEmpty) - if self.reactor?.currentState.currentPage == 0, !self.reactor!.currentState.isBookmarkUpdateOnly { + guard let reactor = owner.reactor else { return } + if reactor.currentState.currentPage == 0, !reactor.currentState.isBookmarkUpdateOnly { collectionView.reloadData() } else { let startIndex = collectionView.numberOfItems(inSection: 0) From 8123542b58252ceb1d29918148b19236c27faada Mon Sep 17 00:00:00 2001 From: p2glet Date: Tue, 9 Dec 2025 20:35:40 +0900 Subject: [PATCH 10/34] =?UTF-8?q?fix/#273:=20=EC=95=8C=EB=A6=BC=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=84=A4=EC=A0=95=20=EC=9C=84=EC=B9=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20/=20=EB=B6=81=EB=A7=88=ED=81=AC=20=EB=B9=84?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8/=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MLS/MLS/Application/AppDelegate.swift | 40 ++++--------- .../OnBoardingNotificationReactor.swift | 4 ++ ...OnBoardingNotificationViewController.swift | 9 ++- .../OnBoardingNotificationSheetReactor.swift | 9 ++- ...rdingNotificationSheetViewController.swift | 30 ++++++++++ .../AuthFeatureDemo/SceneDelegate.swift | 38 ++++++------- .../NotificationPermissionManager.swift | 57 +++++++++++++++++++ .../BookmarkMainFactoryImpl.swift | 8 +-- .../BookmarkMain/BookmarkMainReactor.swift | 10 ++-- .../DictionaryMainReactor.swift | 25 ++++---- .../DictionaryMainViewController.swift | 16 +++++- .../DictionaryMainViewFactoryImpl.swift | 13 +++-- .../DictionaryNotificationFactoryImpl.swift | 1 + .../DictionaryNotificationReactor.swift | 2 + ...DictionaryNotificationViewController.swift | 5 +- .../NotificationSettingReactor.swift | 7 ++- .../NotificationSettingView.swift | 1 - .../NotificationSettingViewController.swift | 43 +++++++++----- 18 files changed, 220 insertions(+), 98 deletions(-) create mode 100644 MLS/Presentation/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index f01266b8..2bba2ec6 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -32,14 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - UserNotification Set FirebaseApp.configure() // Firebase Set Messaging.messaging().delegate = self // 파이어베이스 Meesaging 설정 - UNUserNotificationCenter.current().delegate = self // NotificationCenter Delegate - let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] // 필요한 알림 권한을 설정 - UNUserNotificationCenter.current().requestAuthorization( - options: authOptions, - completionHandler: { _, _ in } - ) - application.registerForRemoteNotifications() // UNUserNotificationCenterDelegate를 구현한 메서드를 실행시킴 // MARK: - Modules Set ImageLoader.shared.configure.diskCacheCountLimit = 10 // ImageLoader @@ -883,31 +876,18 @@ extension AppDelegate { } DIContainer.register(type: DictionaryNotificationFactory.self) { DictionaryNotificationFactoryImpl( - notificationSettingFactory: DIContainer.resolve( - type: NotificationSettingFactory.self - ), - fetchAllAlarmUseCase: DIContainer.resolve( - type: FetchAllAlarmUseCase.self - ), - fetchProfileUseCase: DIContainer.resolve( - type: FetchProfileUseCase.self - ) + notificationSettingFactory: DIContainer.resolve(type: NotificationSettingFactory.self), + fetchAllAlarmUseCase: DIContainer.resolve(type: FetchAllAlarmUseCase.self), + fetchProfileUseCase: DIContainer.resolve(type: FetchProfileUseCase.self) ) } DIContainer.register(type: DictionaryMainViewFactory.self) { DictionaryMainViewFactoryImpl( - dictionaryMainListFactory: - DIContainer - .resolve(type: DictionaryMainListFactory.self), - searchFactory: DIContainer.resolve( - type: DictionarySearchFactory.self - ), - notificationFactory: - DIContainer - .resolve(type: DictionaryNotificationFactory.self), - checkLoginUseCase: DIContainer.resolve( - type: CheckLoginUseCase.self - ) + dictionaryMainListFactory: DIContainer.resolve(type: DictionaryMainListFactory.self), + searchFactory: DIContainer.resolve(type: DictionarySearchFactory.self), + notificationFactory: DIContainer.resolve(type: DictionaryNotificationFactory.self), + loginFactory: DIContainer.resolve(type: LoginFactory.self), + fetchProfileUseCase: DIContainer.resolve(type: FetchProfileUseCase.self) ) } DIContainer.register(type: OnBoardingNotificationSheetFactory.self) { @@ -998,8 +978,8 @@ extension AppDelegate { BookmarkMainFactoryImpl( setBookmarkUseCase: DIContainer .resolve(type: SetBookmarkUseCase.self), - checkLoginUseCase: DIContainer - .resolve(type: CheckLoginUseCase.self), + fetchProfileUseCase: DIContainer + .resolve(type: FetchProfileUseCase.self), onBoardingFactory: DIContainer .resolve(type: BookmarkOnBoardingFactory.self), diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationReactor.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationReactor.swift index 4d4029b4..042dc04e 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationReactor.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationReactor.swift @@ -6,12 +6,14 @@ public final class OnBoardingNotificationReactor: Reactor { public enum Route { case none case notificationAlert + case pop case home } public enum Action { case nextButtonTapped case skipButtonTapped + case backButtonTapped } public enum Mutation { @@ -40,6 +42,8 @@ public final class OnBoardingNotificationReactor: Reactor { return .just(.navigateTo(route: .notificationAlert)) case .skipButtonTapped: return .just(.navigateTo(route: .home)) + case .backButtonTapped: + return .just(.navigateTo(route: .pop)) } } diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift index 62aec0ad..068cdff0 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift @@ -76,6 +76,11 @@ public extension OnBoardingNotificationViewController { .map { Reactor.Action.nextButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) + + mainView.headerView.leftButton.rx.tap + .map { Reactor.Action.backButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) mainView.headerView.underlineTextButton.rx.tap .map { Reactor.Action.skipButtonTapped } @@ -93,9 +98,11 @@ public extension OnBoardingNotificationViewController { switch route { case .notificationAlert: let viewController = owner.onBoardingNotificationSheetFactory.make(selectedLevel: reactor.currentState.selectedLevel, selectedJobID: reactor.currentState.selectedJobID) - owner.presentModal(viewController) + owner.presentModal(viewController, hideTabBar: true) case .home: owner.appCoordinator.showMainTab() + case .pop: + owner.navigationController?.popViewController(animated: true) default: break } diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetReactor.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetReactor.swift index 3c72a9a3..b9e1f8ce 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetReactor.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetReactor.swift @@ -20,12 +20,15 @@ public final class OnBoardingNotificationSheetReactor: Reactor { case cancelButtonTapped case applyButtonTapped case skipButtonTapped + case updateAuthorization(Bool) + case appWillEnterForeground } public enum Mutation { case navigateTo(route: Route) case setLocalNotification(Bool) case setRemoteNotification(Bool) + case setAuthorized(Bool) } public struct State { @@ -64,7 +67,7 @@ public final class OnBoardingNotificationSheetReactor: Reactor { // MARK: - Reactor Methods public func mutate(action: Action) -> Observable { switch action { - case .viewWillAppear: + case .viewWillAppear, .appWillEnterForeground: return checkNotificationPermissionUseCase.execute() .asObservable() .map { .setLocalNotification($0) } @@ -86,6 +89,8 @@ public final class OnBoardingNotificationSheetReactor: Reactor { return .just(.navigateTo(route: .dismiss)) case .skipButtonTapped: return .just(.navigateTo(route: .home)) + case .updateAuthorization(let authorized): + return .just(.setAuthorized(authorized)) } } @@ -99,6 +104,8 @@ public final class OnBoardingNotificationSheetReactor: Reactor { newState.isAgreeLocalNotification = isAgree case .setRemoteNotification(let isAgree): newState.isAgreeRemoteNotification = isAgree + case let .setAuthorized(isAuthorized): + newState.isAgreeLocalNotification = isAuthorized } return newState diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift index 2c2ed762..c67b31ca 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift @@ -61,9 +61,17 @@ extension OnBoardingNotificationSheetViewController { func bindUserActions(reactor: Reactor) { rx.viewWillAppear .take(1) + .do(onNext: { [weak self] _ in + self?.checkNotificationAuthorization() + }) .map { _ in Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) + + NotificationCenter.default.rx.notification(UIApplication.willEnterForegroundNotification) + .map { _ in Reactor.Action.appWillEnterForeground } + .bind(to: reactor.action) + .disposed(by: disposeBag) mainView.header.firstIconButton.rx.tap .map { Reactor.Action.cancelButtonTapped } @@ -125,3 +133,25 @@ extension OnBoardingNotificationSheetViewController { .disposed(by: disposeBag) } } + +// MARK: - Notification Authorization +private extension OnBoardingNotificationSheetViewController { + func checkNotificationAuthorization() { + guard let reactor = reactor else { return } + + NotificationPermissionManager.shared.getStatus { status in + switch status { + case .authorized, .provisional: + reactor.action.onNext(.updateAuthorization(true)) + case .denied: + reactor.action.onNext(.updateAuthorization(false)) + case .notDetermined: + NotificationPermissionManager.shared.requestIfNeeded { granted in + reactor.action.onNext(.updateAuthorization(granted)) + } + default: + reactor.action.onNext(.updateAuthorization(false)) + } + } + } +} diff --git a/MLS/Presentation/AuthFeature/AuthFeatureDemo/SceneDelegate.swift b/MLS/Presentation/AuthFeature/AuthFeatureDemo/SceneDelegate.swift index 8ebd28c1..228440bb 100644 --- a/MLS/Presentation/AuthFeature/AuthFeatureDemo/SceneDelegate.swift +++ b/MLS/Presentation/AuthFeature/AuthFeatureDemo/SceneDelegate.swift @@ -13,26 +13,26 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.rootViewController = UINavigationController(rootViewController: ViewController()) window?.makeKeyAndVisible() - checkNotificationPermission() - } - - private func checkNotificationPermission() { - UNUserNotificationCenter.current().getNotificationSettings { settings in - switch settings.authorizationStatus { - case .notDetermined: - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, error in - if let error = error { - print(error) - return - } - } - case .denied, .authorized, .provisional, .ephemeral: - print(settings.authorizationStatus) - @unknown default: - break - } - } +// checkNotificationPermission() } +// +// private func checkNotificationPermission() { +// UNUserNotificationCenter.current().getNotificationSettings { settings in +// switch settings.authorizationStatus { +// case .notDetermined: +// UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, error in +// if let error = error { +// print(error) +// return +// } +// } +// case .denied, .authorized, .provisional, .ephemeral: +// print(settings.authorizationStatus) +// @unknown default: +// break +// } +// } +// } func sceneDidDisconnect(_ scene: UIScene) {} diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift new file mode 100644 index 00000000..a437f5b6 --- /dev/null +++ b/MLS/Presentation/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift @@ -0,0 +1,57 @@ +import UIKit +import UserNotifications + +public final class NotificationPermissionManager { + + public static let shared = NotificationPermissionManager() + private init() {} + + public func getStatus(completion: @escaping (UNAuthorizationStatus) -> Void) { + UNUserNotificationCenter.current().getNotificationSettings { settings in + completion(settings.authorizationStatus) + } + } + + @discardableResult + public func requestIfNeeded( + application: UIApplication = .shared, + completion: ((Bool) -> Void)? = nil + ) -> Void { + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { settings in + switch settings.authorizationStatus { + case .notDetermined: + center.requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in + if let error = error { + print("error: \(error.localizedDescription)") + completion?(false) + return + } + if granted { + DispatchQueue.main.async { + application.registerForRemoteNotifications() + } + print("알림 권한 허용") + completion?(true) + } else { + print("알림 권한 거부") + completion?(false) + } + } + + case .authorized, .provisional: + DispatchQueue.main.async { + application.registerForRemoteNotifications() + } + completion?(true) + + case .denied: + print("🚫 알림 권한 거부 상태입니다. 설정에서 변경해야 함") + completion?(false) + + @unknown default: + completion?(false) + } + } + } +} diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift index 76e5ed3e..a8e5d62e 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift @@ -6,7 +6,7 @@ import DomainInterface public final class BookmarkMainFactoryImpl: BookmarkMainFactory { private let setBookmarkUseCase: SetBookmarkUseCase - private let checkLoginUseCase: CheckLoginUseCase + private let fetchProfileUseCase: FetchProfileUseCase private let onBoardingFactory: BookmarkOnBoardingFactory private let bookmarkListFactory: BookmarkListFactory @@ -17,7 +17,7 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { public init( setBookmarkUseCase: SetBookmarkUseCase, - checkLoginUseCase: CheckLoginUseCase, + fetchProfileUseCase: FetchProfileUseCase, onBoardingFactory: BookmarkOnBoardingFactory, bookmarkListFactory: BookmarkListFactory, collectionListFactory: CollectionListFactory, @@ -26,7 +26,7 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { loginFactory: LoginFactory ) { self.setBookmarkUseCase = setBookmarkUseCase - self.checkLoginUseCase = checkLoginUseCase + self.fetchProfileUseCase = fetchProfileUseCase self.onBoardingFactory = onBoardingFactory self.bookmarkListFactory = bookmarkListFactory self.collectionListFactory = collectionListFactory @@ -37,7 +37,7 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { public func make() -> BaseViewController { let reactor = BookmarkMainReactor( - setBookmarkUseCase: setBookmarkUseCase, checkLoginUseCase: checkLoginUseCase + setBookmarkUseCase: setBookmarkUseCase, fetchProfileUseCase: fetchProfileUseCase ) let viewController = BookmarkMainViewController( onBoardingFactory: onBoardingFactory, diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift index 5b43f8b5..0d692140 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift @@ -35,23 +35,23 @@ public final class BookmarkMainReactor: Reactor { // MARK: - Properties private let setBookmarkUseCase: SetBookmarkUseCase - private let checkLoginUseCase: CheckLoginUseCase + private let fetchProfileUseCase: FetchProfileUseCase public var initialState: State private let disposeBag = DisposeBag() - public init(setBookmarkUseCase: SetBookmarkUseCase, checkLoginUseCase: CheckLoginUseCase) { + public init(setBookmarkUseCase: SetBookmarkUseCase, fetchProfileUseCase: FetchProfileUseCase) { self.initialState = State(route: .none) self.setBookmarkUseCase = setBookmarkUseCase - self.checkLoginUseCase = checkLoginUseCase + self.fetchProfileUseCase = fetchProfileUseCase } public func mutate(action: Action) -> Observable { switch action { case .viewWillAppear: - return checkLoginUseCase.execute() - .map { .setLogin($0) } + return fetchProfileUseCase.execute() + .map { .setLogin($0 != nil) } case .searchButtonTapped: return Observable.just(.navigateTo(.search)) case .notificationButtonTapped: diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift index 54286068..564ab45b 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift @@ -12,12 +12,14 @@ public final class DictionaryMainReactor: Reactor { } public enum Action { + case viewWillAppear case searchButtonTapped case notificationButtonTapped } public enum Mutation { case navigateTo(Route) + case setLogin(Bool) } public struct State { @@ -26,34 +28,31 @@ public final class DictionaryMainReactor: Reactor { var sections: [String] { return type.pageTabList.map { $0.title } } + var isLogin = false } // MARK: - properties public var initialState: State var disposeBag = DisposeBag() - private let checkLoginUseCase: CheckLoginUseCase + private let fetchProfileUseCase: FetchProfileUseCase // MARK: - init - public init(checkLoginUseCase: CheckLoginUseCase) { + public init(fetchProfileUseCase: FetchProfileUseCase) { self.initialState = State() - self.checkLoginUseCase = checkLoginUseCase + self.fetchProfileUseCase = fetchProfileUseCase } // MARK: - Reactor Methods public func mutate(action: Action) -> Observable { switch action { + case .viewWillAppear: + return fetchProfileUseCase.execute() + .map { .setLogin($0 != nil) } case .searchButtonTapped: - return Observable.just(.navigateTo(.search)) + return .just(.navigateTo(.search)) case .notificationButtonTapped: - return checkLoginUseCase.execute() - .map { isLogin in - if isLogin { - return .navigateTo(.notification) - } else { - return .navigateTo(.login) - } - } + return .just(.navigateTo(currentState.isLogin ? .notification : .login)) } } @@ -63,6 +62,8 @@ public final class DictionaryMainReactor: Reactor { switch mutation { case .navigateTo(let route): newState.route = route + case let .setLogin(isLogin): + newState.isLogin = isLogin } return newState diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift index dd969c9c..86b2a97f 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift @@ -1,5 +1,6 @@ import UIKit +import AuthFeatureInterface import BaseFeature import DesignSystem import DictionaryFeatureInterface @@ -20,6 +21,7 @@ public final class DictionaryMainViewController: BaseViewController, View { private let searchFactory: DictionarySearchFactory private let notificationFactory: DictionaryNotificationFactory + private let loginFactory: LoginFactory private var viewControllers: [UIViewController] @@ -31,6 +33,7 @@ public final class DictionaryMainViewController: BaseViewController, View { dictionaryMainListFactory: DictionaryMainListFactory, searchFactory: DictionarySearchFactory, notificationFactory: DictionaryNotificationFactory, + loginFactory: LoginFactory, reactor: DictionaryMainReactor ) { let type = reactor.currentState.type @@ -38,6 +41,7 @@ public final class DictionaryMainViewController: BaseViewController, View { self.viewControllers = type.pageTabList.map { dictionaryMainListFactory.make(type: $0, listType: type, keyword: "") } self.searchFactory = searchFactory self.notificationFactory = notificationFactory + self.loginFactory = loginFactory self.initialIndex = initialIndex super.init() self.reactor = reactor @@ -120,6 +124,11 @@ public extension DictionaryMainViewController { } func bindUserActions(reactor: Reactor) { + rx.viewWillAppear + .map { .viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + mainView.headerView.firstIconButton.rx.tap .map { Reactor.Action.searchButtonTapped } .bind(to: reactor.action) @@ -134,9 +143,9 @@ public extension DictionaryMainViewController { func bindViewState(reactor: Reactor) { rx.viewDidAppear .take(1) - .flatMapLatest { _ in return reactor.pulse(\.$route) } - .withUnretained(self) + .flatMapLatest { _ in reactor.pulse(\.$route) } .observe(on: MainScheduler.instance) + .withUnretained(self) .subscribe { (owner, route) in switch route { case .search: @@ -145,6 +154,9 @@ public extension DictionaryMainViewController { case .notification: let controller = owner.notificationFactory.make() owner.navigationController?.pushViewController(controller, animated: true) + case .login: + let controller = owner.loginFactory.make(exitRoute: .pop, onLoginCompleted: nil) + owner.navigationController?.pushViewController(controller, animated: true) default: break } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewFactoryImpl.swift index d490c9a3..798c5e6f 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewFactoryImpl.swift @@ -1,3 +1,4 @@ +import AuthFeatureInterface import BaseFeature import DictionaryFeatureInterface import DomainInterface @@ -6,18 +7,20 @@ public final class DictionaryMainViewFactoryImpl: DictionaryMainViewFactory { private let dictionaryMainListFactory: DictionaryMainListFactory private let searchFactory: DictionarySearchFactory private let notificationFactory: DictionaryNotificationFactory - private let checkLoginUseCase: CheckLoginUseCase + private let loginFactory: LoginFactory + private let fetchProfileUseCase: FetchProfileUseCase - public init(dictionaryMainListFactory: DictionaryMainListFactory, searchFactory: DictionarySearchFactory, notificationFactory: DictionaryNotificationFactory, checkLoginUseCase: CheckLoginUseCase) { + public init(dictionaryMainListFactory: DictionaryMainListFactory, searchFactory: DictionarySearchFactory, notificationFactory: DictionaryNotificationFactory, loginFactory: LoginFactory, fetchProfileUseCase: FetchProfileUseCase) { self.dictionaryMainListFactory = dictionaryMainListFactory self.searchFactory = searchFactory self.notificationFactory = notificationFactory - self.checkLoginUseCase = checkLoginUseCase + self.loginFactory = loginFactory + self.fetchProfileUseCase = fetchProfileUseCase } public func make() -> BaseViewController { - let reactor = DictionaryMainReactor(checkLoginUseCase: checkLoginUseCase) - let viewController = DictionaryMainViewController(dictionaryMainListFactory: dictionaryMainListFactory, searchFactory: searchFactory, notificationFactory: notificationFactory, reactor: reactor) + let reactor = DictionaryMainReactor(fetchProfileUseCase: fetchProfileUseCase) + let viewController = DictionaryMainViewController(dictionaryMainListFactory: dictionaryMainListFactory, searchFactory: searchFactory, notificationFactory: notificationFactory, loginFactory: loginFactory, reactor: reactor, ) viewController.isBottomTabbarHidden = false viewController.reactor = reactor return viewController diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationFactoryImpl.swift index d7875a36..bfaa367c 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationFactoryImpl.swift @@ -5,6 +5,7 @@ import MyPageFeatureInterface public final class DictionaryNotificationFactoryImpl: DictionaryNotificationFactory { private let notificationSettingFactory: NotificationSettingFactory + private let fetchAllAlarmUseCase: FetchAllAlarmUseCase private let fetchProfileUseCase: FetchProfileUseCase diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationReactor.swift index 36a40a99..1e3e3653 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationReactor.swift @@ -52,6 +52,8 @@ public final class DictionaryNotificationReactor: Reactor { switch action { case .viewWillAppear: return .concat([ + fetchProfileUseCase.execute() + .map { .setProfile($0) }, .just(.setLoading(true)), fetchAllAlarmUseCase.execute(cursor: nil, pageSize: 20) .map { paged in diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift index da8b6589..baa81a69 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift @@ -107,7 +107,7 @@ public extension DictionaryNotificationViewController { case .dismiss: owner.navigationController?.popViewController(animated: true) case .setting: - guard let reactor = owner.reactor, + guard let reactor = owner.reactor , let profile = reactor.currentState.profile else { return } let viewController = owner.notificationSettingFactory.make(isAgreeEventNotification: profile.eventAgreement, isAgreeNoticeNotification: profile.noticeAgreement, isAgreePatchNoteNotification: profile.patchNoteAgreement) owner.navigationController?.pushViewController(viewController, animated: true) @@ -124,7 +124,8 @@ public extension DictionaryNotificationViewController { .subscribe { owner, _ in owner.mainView.notificationCollectionView.reloadData() } - .disposed(by: disposeBag) } + .disposed(by: disposeBag) + } } // MARK: - Delegate diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingReactor.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingReactor.swift index 6596009d..b36a593d 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingReactor.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingReactor.swift @@ -17,6 +17,7 @@ public final class NotificationSettingReactor: Reactor { case noticeViewSwitch(Bool) case patchNoteViewSwitch(Bool) case pushGuideViewTapped + case updateAuthorization(Bool) } public enum Mutation { @@ -44,9 +45,7 @@ public final class NotificationSettingReactor: Reactor { init(checkNotificationPermissionUseCase: CheckNotificationPermissionUseCase, updateNotificationAgreementUseCase: UpdateNotificationAgreementUseCase, isAgreeEventNotification: Bool, isAgreeNoticeNotification: Bool, - isAgreePatchNoteNotification: Bool - - ) { + isAgreePatchNoteNotification: Bool) { self.initialState = .init(isAgreeEventNotification: isAgreeEventNotification, isAgreeNoticeNotification: isAgreeNoticeNotification, isAgreePatchNoteNotification: isAgreePatchNoteNotification) self.checkNotificationPermissionUseCase = checkNotificationPermissionUseCase self.updateNotificationAgreementUseCase = updateNotificationAgreementUseCase @@ -71,6 +70,8 @@ public final class NotificationSettingReactor: Reactor { .andThen(.just(.setPatchNoteNotification(isAgree))) case .pushGuideViewTapped: return .just(.toNavigate(.setting)) + case .updateAuthorization(let authorized): + return .just(.setAuthorized(authorized)) } } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingView.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingView.swift index b4e3c348..7c0612bc 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingView.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingView.swift @@ -14,7 +14,6 @@ final class NotificationSettingView: UIView { static let iconInset: CGFloat = 10 static let buttonSize: CGFloat = 44 static let topMargin: CGFloat = 20 -// static let horizontalMargin: CGFloat = 16 } // MARK: - Components diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingViewController.swift index 1b936661..6a0d9b4a 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/NotificationSetting/NotificationSettingViewController.swift @@ -31,7 +31,6 @@ final class NotificationSettingViewController: BaseViewController, View, UNUserN override func viewDidLoad() { super.viewDidLoad() setupUI() - bindNotification() } } @@ -49,13 +48,23 @@ private extension NotificationSettingViewController { // MARK: - Notification Authorization private extension NotificationSettingViewController { - func bindNotification() { + func checkNotificationAuthorization() { guard let reactor = reactor else { return } - NotificationCenter.default.rx.notification(UIApplication.willEnterForegroundNotification) - .observe(on: MainScheduler.instance) - .map { _ in NotificationSettingReactor.Action.appWillEnterForeground } - .bind(to: reactor.action) - .disposed(by: disposeBag) + + NotificationPermissionManager.shared.getStatus { status in + switch status { + case .authorized, .provisional: + reactor.action.onNext(.updateAuthorization(true)) + case .denied: + reactor.action.onNext(.updateAuthorization(false)) + case .notDetermined: + NotificationPermissionManager.shared.requestIfNeeded { granted in + reactor.action.onNext(.updateAuthorization(granted)) + } + default: + reactor.action.onNext(.updateAuthorization(false)) + } + } } } @@ -67,6 +76,20 @@ extension NotificationSettingViewController { } private func bindUserActions(reactor: Reactor) { + rx.viewWillAppear + .take(1) + .do(onNext: { [weak self] _ in + self?.checkNotificationAuthorization() + }) + .map { _ in Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + NotificationCenter.default.rx.notification(UIApplication.willEnterForegroundNotification) + .map { _ in Reactor.Action.appWillEnterForeground } + .bind(to: reactor.action) + .disposed(by: disposeBag) + mainView.backButton.rx.tap .map { Reactor.Action.backButtonTapped } .bind(to: reactor.action) @@ -97,12 +120,6 @@ extension NotificationSettingViewController { } private func bindViewState(reactor: Reactor) { - rx.viewWillAppear - .take(1) - .map { _ in Reactor.Action.viewWillAppear } - .bind(to: reactor.action) - .disposed(by: disposeBag) - reactor.state .observe(on: MainScheduler.instance) .map { $0.authorized } From 4c3d1dc8bf91c6132288843cf96b6cf1f38f839f Mon Sep 17 00:00:00 2001 From: p2glet Date: Tue, 9 Dec 2025 20:40:27 +0900 Subject: [PATCH 11/34] =?UTF-8?q?fix/#273:=20AddFolderCell=20=ED=84=B0?= =?UTF-8?q?=EC=B9=98=EC=98=81=EC=97=AD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionViewCells/AddFolderCell.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/AddFolderCell.swift b/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/AddFolderCell.swift index 6bef91bb..5632d0ad 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/AddFolderCell.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/AddFolderCell.swift @@ -15,16 +15,16 @@ public class AddFolderCell: UICollectionViewCell { } // MARK: - Components - private lazy var addButton: UIButton = { - let button = UIButton() - button.layer.cornerRadius = Constant.radius - button.backgroundColor = .primary100 + private lazy var addIconView: UIView = { + let view = UIView() + view.layer.cornerRadius = Constant.radius + view.backgroundColor = .primary100 - button.addSubview(iconView) + view.addSubview(iconView) iconView.snp.makeConstraints { make in - make.center.equalTo(button).inset(Constant.iconInset) + make.center.equalTo(view).inset(Constant.iconInset) } - return button + return view }() private let iconView: UIImageView = { @@ -61,19 +61,19 @@ public class AddFolderCell: UICollectionViewCell { // MARK: - SetUp private extension AddFolderCell { func addViews() { - contentView.addSubview(addButton) + contentView.addSubview(addIconView) contentView.addSubview(titleLabel) contentView.addSubview(divider) } func setupConstraints() { - addButton.snp.makeConstraints { make in + addIconView.snp.makeConstraints { make in make.leading.verticalEdges.equalToSuperview().inset(Constant.margin) make.size.equalTo(Constant.buttonSize) } titleLabel.snp.makeConstraints { make in - make.leading.equalTo(addButton.snp.trailing).offset(Constant.margin) + make.leading.equalTo(addIconView.snp.trailing).offset(Constant.margin) make.centerY.equalToSuperview() make.trailing.equalToSuperview().inset(Constant.margin) } From 68617c7d465a5772b958c35d3deb3478541bdc3e Mon Sep 17 00:00:00 2001 From: p2glet Date: Tue, 9 Dec 2025 23:55:14 +0900 Subject: [PATCH 12/34] =?UTF-8?q?feat/#273:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=ED=95=98=EB=9F=AC=EA=B0=80=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EC=8B=9C=20=EB=8F=84=EA=B0=90=20=ED=83=AD=20?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthAPI/CheckLoginUseCaseImpl.swift | 19 ------ .../DictionaryList/DictionaryType.swift | 19 ++++++ MLS/MLS/Application/AppDelegate.swift | 9 +-- .../Interface/DictionaryTabControllable.swift | 3 + .../Utills/DictionaryTabRegistry.swift | 11 +++ .../BookmarkListFactoryImpl.swift | 8 +-- .../BookmarkList/BookmarkListReactor.swift | 12 ++-- .../BookmarkListViewController.swift | 1 + .../BookmarkMainFactoryImpl.swift | 11 +-- .../BookmarkMain/BookmarkMainReactor.swift | 14 ++-- .../BookmarkMain/BookmarkMainView.swift | 31 +++++---- .../DictionaryMainReactor.swift | 7 ++ .../DictionaryMainViewController.swift | 67 +++++++++++++------ 13 files changed, 138 insertions(+), 74 deletions(-) create mode 100644 MLS/Presentation/BaseFeature/BaseFeature/Interface/DictionaryTabControllable.swift create mode 100644 MLS/Presentation/BaseFeature/BaseFeature/Utills/DictionaryTabRegistry.swift diff --git a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift index 1993ad45..5b285263 100644 --- a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift @@ -6,17 +6,6 @@ import RxSwift public final class CheckLoginUseCaseImpl: CheckLoginUseCase { private let authRepository: AuthAPIRepository private let tokenRepository: TokenRepository - private let disposeBag = DisposeBag() - - private let loginCheckRelay = PublishRelay() - private lazy var sharedLoginCheck: Observable = { - loginCheckRelay - .flatMapLatest { [weak self] _ -> Observable in - guard let self else { return .just(false) } - return self.executeInternal() - } - .share(replay: 1, scope: .forever) - }() public init(authRepository: AuthAPIRepository, tokenRepository: TokenRepository) { self.authRepository = authRepository @@ -24,14 +13,6 @@ public final class CheckLoginUseCaseImpl: CheckLoginUseCase { } public func execute() -> Observable { - return Observable.deferred { [weak self] in - guard let self else { return .just(false) } - self.loginCheckRelay.accept(()) - return self.sharedLoginCheck - } - } - - private func executeInternal() -> Observable { switch tokenRepository.fetchToken(type: .refreshToken) { case .success(let token): guard !token.isEmpty else { return .just(false) } diff --git a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift index 51d441f5..cb164b00 100644 --- a/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift +++ b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift @@ -107,4 +107,23 @@ public enum DictionaryType: String, CaseIterable { return nil } } + + public var tabIndex: Int { + switch self { + case .total: + 0 + case .collection: + 0 + case .item: + 1 + case .monster: + 2 + case .map: + 3 + case .npc: + 4 + case .quest: + 5 + } + } } diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index 2bba2ec6..30d29609 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -978,8 +978,9 @@ extension AppDelegate { BookmarkMainFactoryImpl( setBookmarkUseCase: DIContainer .resolve(type: SetBookmarkUseCase.self), - fetchProfileUseCase: DIContainer - .resolve(type: FetchProfileUseCase.self), +// fetchProfileUseCase: DIContainer +// .resolve(type: FetchProfileUseCase.self), + checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), onBoardingFactory: DIContainer .resolve(type: BookmarkOnBoardingFactory.self), @@ -1024,8 +1025,8 @@ extension AppDelegate { setBookmarkUseCase: DIContainer.resolve( type: SetBookmarkUseCase.self ), - checkLoginUseCase: DIContainer.resolve( - type: CheckLoginUseCase.self + fetchProfileUseCase: DIContainer.resolve( + type: FetchProfileUseCase.self ), fetchBookmarkUseCase: DIContainer.resolve( type: FetchBookmarkUseCase.self diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Interface/DictionaryTabControllable.swift b/MLS/Presentation/BaseFeature/BaseFeature/Interface/DictionaryTabControllable.swift new file mode 100644 index 00000000..1f0e5479 --- /dev/null +++ b/MLS/Presentation/BaseFeature/BaseFeature/Interface/DictionaryTabControllable.swift @@ -0,0 +1,3 @@ +public protocol DictionaryTabControllable: AnyObject { + func changeTab(index: Int) +} diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Utills/DictionaryTabRegistry.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/DictionaryTabRegistry.swift new file mode 100644 index 00000000..467443c7 --- /dev/null +++ b/MLS/Presentation/BaseFeature/BaseFeature/Utills/DictionaryTabRegistry.swift @@ -0,0 +1,11 @@ +public enum DictionaryTabRegistry { + private static weak var controller: DictionaryTabControllable? + + public static func register(controller: DictionaryTabControllable) { + self.controller = controller + } + + public static func changeTab(index: Int) { + controller?.changeTab(index: index) + } +} diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListFactoryImpl.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListFactoryImpl.swift index 74661da1..edb8ceb6 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListFactoryImpl.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListFactoryImpl.swift @@ -14,7 +14,7 @@ public final class BookmarkListFactoryImpl: BookmarkListFactory { private let collectionEditFactory: CollectionEditFactory private let setBookmarkUseCase: SetBookmarkUseCase - private let checkLoginUseCase: CheckLoginUseCase + private let fetchProfileUseCase: FetchProfileUseCase private let fetchBookmarkUseCase: FetchBookmarkUseCase private let fetchMonsterBookmarkUseCase: FetchMonsterBookmarkUseCase private let fetchItemBookmarkUseCase: FetchItemBookmarkUseCase @@ -32,7 +32,7 @@ public final class BookmarkListFactoryImpl: BookmarkListFactory { dictionaryDetailFactory: DictionaryDetailFactory, collectionEditFactory: CollectionEditFactory, setBookmarkUseCase: SetBookmarkUseCase, - checkLoginUseCase: CheckLoginUseCase, + fetchProfileUseCase: FetchProfileUseCase, fetchBookmarkUseCase: FetchBookmarkUseCase, fetchMonsterBookmarkUseCase: FetchMonsterBookmarkUseCase, fetchItemBookmarkUseCase: FetchItemBookmarkUseCase, @@ -49,7 +49,7 @@ public final class BookmarkListFactoryImpl: BookmarkListFactory { self.dictionaryDetailFactory = dictionaryDetailFactory self.collectionEditFactory = collectionEditFactory self.setBookmarkUseCase = setBookmarkUseCase - self.checkLoginUseCase = checkLoginUseCase + self.fetchProfileUseCase = fetchProfileUseCase self.fetchBookmarkUseCase = fetchBookmarkUseCase self.fetchNPCBookmarkUseCase = fetchNPCBookmarkUseCase self.fetchMonsterBookmarkUseCase = fetchMonsterBookmarkUseCase @@ -62,7 +62,7 @@ public final class BookmarkListFactoryImpl: BookmarkListFactory { public func make(type: DictionaryType, listType: DictionaryMainViewType) -> BaseViewController { let reactor = BookmarkListReactor( type: type, - checkLoginUseCase: checkLoginUseCase, + fetchProfileUseCase: fetchProfileUseCase, setBookmarkUseCase: setBookmarkUseCase, fetchBookmarkUseCase: fetchBookmarkUseCase, fetchMonsterBookmarkUseCase: fetchMonsterBookmarkUseCase, diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListReactor.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListReactor.swift index 6ca97d98..de507057 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListReactor.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListReactor.swift @@ -75,7 +75,7 @@ public final class BookmarkListReactor: Reactor { public var initialState: State // MARK: - UseCases - private let checkLoginUseCase: CheckLoginUseCase + private let fetchProfileUseCase: FetchProfileUseCase private let setBookmarkUseCase: SetBookmarkUseCase private let fetchTotalBookmarkUseCase: FetchBookmarkUseCase @@ -91,7 +91,7 @@ public final class BookmarkListReactor: Reactor { // MARK: - Init public init( type: DictionaryType, - checkLoginUseCase: CheckLoginUseCase, + fetchProfileUseCase: FetchProfileUseCase, setBookmarkUseCase: SetBookmarkUseCase, fetchBookmarkUseCase: FetchBookmarkUseCase, fetchMonsterBookmarkUseCase: FetchMonsterBookmarkUseCase, @@ -102,7 +102,7 @@ public final class BookmarkListReactor: Reactor { parseItemFilterResultUseCase: ParseItemFilterResultUseCase ) { self.initialState = State(route: .none, type: type, isLogin: false) - self.checkLoginUseCase = checkLoginUseCase + self.fetchProfileUseCase = fetchProfileUseCase self.setBookmarkUseCase = setBookmarkUseCase self.fetchTotalBookmarkUseCase = fetchBookmarkUseCase self.fetchMonsterBookmarkUseCase = fetchMonsterBookmarkUseCase @@ -117,10 +117,10 @@ public final class BookmarkListReactor: Reactor { public func mutate(action: Action) -> Observable { switch action { case .viewWillAppear: - return checkLoginUseCase.execute() - .flatMap { [weak self] isLoggedIn -> Observable in + return fetchProfileUseCase.execute() + .flatMap { [weak self] profile -> Observable in guard let self = self else { return .empty() } - if !isLoggedIn { + if profile == nil { return .just(.setLoginState(false)) } else { return Observable.concat([ diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift index d8316456..d0ec0e6c 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkList/BookmarkListViewController.swift @@ -189,6 +189,7 @@ extension BookmarkListViewController { case .dictionary: if let tabBarController = owner.tabBarController as? BottomTabBarController { tabBarController.selectTab(index: 0) + DictionaryTabRegistry.changeTab(index: reactor.currentState.type.tabIndex) } case .edit: let viewController = owner.collectionEditFactory.make(bookmarks: reactor.currentState.items) diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift index a8e5d62e..993a7524 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift @@ -6,7 +6,8 @@ import DomainInterface public final class BookmarkMainFactoryImpl: BookmarkMainFactory { private let setBookmarkUseCase: SetBookmarkUseCase - private let fetchProfileUseCase: FetchProfileUseCase +// private let fetchProfileUseCase: FetchProfileUseCase + private let checkLoginUseCase: CheckLoginUseCase private let onBoardingFactory: BookmarkOnBoardingFactory private let bookmarkListFactory: BookmarkListFactory @@ -17,7 +18,8 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { public init( setBookmarkUseCase: SetBookmarkUseCase, - fetchProfileUseCase: FetchProfileUseCase, +// fetchProfileUseCase: FetchProfileUseCase, + checkLoginUseCase: CheckLoginUseCase, onBoardingFactory: BookmarkOnBoardingFactory, bookmarkListFactory: BookmarkListFactory, collectionListFactory: CollectionListFactory, @@ -26,7 +28,8 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { loginFactory: LoginFactory ) { self.setBookmarkUseCase = setBookmarkUseCase - self.fetchProfileUseCase = fetchProfileUseCase +// self.fetchProfileUseCase = fetchProfileUseCase + self.checkLoginUseCase = checkLoginUseCase self.onBoardingFactory = onBoardingFactory self.bookmarkListFactory = bookmarkListFactory self.collectionListFactory = collectionListFactory @@ -37,7 +40,7 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { public func make() -> BaseViewController { let reactor = BookmarkMainReactor( - setBookmarkUseCase: setBookmarkUseCase, fetchProfileUseCase: fetchProfileUseCase + setBookmarkUseCase: setBookmarkUseCase, /*fetchProfileUseCase: fetchProfileUseCase*/checkLoginUseCase: checkLoginUseCase ) let viewController = BookmarkMainViewController( onBoardingFactory: onBoardingFactory, diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift index 0d692140..6712a5c0 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift @@ -35,23 +35,27 @@ public final class BookmarkMainReactor: Reactor { // MARK: - Properties private let setBookmarkUseCase: SetBookmarkUseCase - private let fetchProfileUseCase: FetchProfileUseCase +// private let fetchProfileUseCase: FetchProfileUseCase + private let checkLoginUseCase: CheckLoginUseCase public var initialState: State private let disposeBag = DisposeBag() - public init(setBookmarkUseCase: SetBookmarkUseCase, fetchProfileUseCase: FetchProfileUseCase) { + public init(setBookmarkUseCase: SetBookmarkUseCase, checkLoginUseCase: CheckLoginUseCase /*fetchProfileUseCase: FetchProfileUseCase*/) { self.initialState = State(route: .none) self.setBookmarkUseCase = setBookmarkUseCase - self.fetchProfileUseCase = fetchProfileUseCase +// self.fetchProfileUseCase = fetchProfileUseCase + self.checkLoginUseCase = checkLoginUseCase } public func mutate(action: Action) -> Observable { switch action { case .viewWillAppear: - return fetchProfileUseCase.execute() - .map { .setLogin($0 != nil) } +// return fetchProfileUseCase.execute() +// .map { .setLogin($0 != nil) } + return checkLoginUseCase.execute() + .map { .setLogin($0) } case .searchButtonTapped: return Observable.just(.navigateTo(.search)) case .notificationButtonTapped: diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift index e2fe8853..f58e0cfa 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift @@ -57,19 +57,10 @@ private extension BookmarkMainView { make.top.equalTo(safeAreaLayoutGuide) make.horizontalEdges.equalToSuperview() } - } - } -} - -// MARK: - Public Update -extension BookmarkMainView { - public func updateLoginState(isLogin: Bool) { - // 기존 서브뷰 제거 - [tabCollectionView, pageViewController.view, emptyView].forEach { $0.removeFromSuperview() } - if isLogin { addSubview(tabCollectionView) addSubview(pageViewController.view) + addSubview(emptyView) tabCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(Constant.topMargin) @@ -82,13 +73,29 @@ extension BookmarkMainView { make.horizontalEdges.equalTo(safeAreaLayoutGuide) make.bottom.equalToSuperview().inset(Constant.bottomTabHeight) } - } else { - addSubview(emptyView) + emptyView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(Constant.topMargin) make.horizontalEdges.equalToSuperview() make.bottom.equalToSuperview().inset(Constant.bottomTabHeight) } + + tabCollectionView.isHidden = true + pageViewController.view.isHidden = true + emptyView.isHidden = false } } } + +// MARK: - Public Update +extension BookmarkMainView { + public func updateLoginState(isLogin: Bool) { + tabCollectionView.isHidden = !isLogin + pageViewController.view.isHidden = !isLogin + + emptyView.isHidden = isLogin + + tabCollectionView.isUserInteractionEnabled = isLogin + pageViewController.view.isUserInteractionEnabled = isLogin + } +} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift index 564ab45b..02730ebb 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift @@ -15,11 +15,13 @@ public final class DictionaryMainReactor: Reactor { case viewWillAppear case searchButtonTapped case notificationButtonTapped + case changeTab(Int) } public enum Mutation { case navigateTo(Route) case setLogin(Bool) + case setCurrentTab(Int) } public struct State { @@ -29,6 +31,7 @@ public final class DictionaryMainReactor: Reactor { return type.pageTabList.map { $0.title } } var isLogin = false + var currentPageIndex = 0 } // MARK: - properties @@ -53,6 +56,8 @@ public final class DictionaryMainReactor: Reactor { return .just(.navigateTo(.search)) case .notificationButtonTapped: return .just(.navigateTo(currentState.isLogin ? .notification : .login)) + case let .changeTab(index): + return .just(.setCurrentTab(index)) } } @@ -64,6 +69,8 @@ public final class DictionaryMainReactor: Reactor { newState.route = route case let .setLogin(isLogin): newState.isLogin = isLogin + case let .setCurrentTab(index): + newState.currentPageIndex = index } return newState diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift index 86b2a97f..b3eaa691 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift @@ -10,14 +10,14 @@ import ReactorKit import RxCocoa import RxSwift -public final class DictionaryMainViewController: BaseViewController, View { +public final class DictionaryMainViewController: BaseViewController, View, DictionaryTabControllable { public typealias Reactor = DictionaryMainReactor // MARK: - Properties public var disposeBag = DisposeBag() private let initialIndex: Int - private lazy var currentPageIndex = BehaviorRelay(value: initialIndex) +// private lazy var currentPageIndex = BehaviorRelay(value: initialIndex) private let searchFactory: DictionarySearchFactory private let notificationFactory: DictionaryNotificationFactory @@ -61,6 +61,7 @@ public extension DictionaryMainViewController { setupConstraints() configureUI() setInitialIndex() + DictionaryTabRegistry.register(controller: self) } } @@ -114,6 +115,29 @@ private extension DictionaryMainViewController { self?.underLineController.setInitialIndicator() } } + + func moveToTab(index: Int) { + guard index < viewControllers.count, + let reactor = reactor else { return } + + let oldIndex = reactor.currentState.currentPageIndex + let direction: UIPageViewController.NavigationDirection = index > oldIndex ? .forward : .reverse + + mainView.pageViewController.setViewControllers( + [viewControllers[index]], + direction: direction, + animated: true, + completion: nil + ) + + mainView.tabCollectionView.selectItem( + at: IndexPath(item: index, section: 0), + animated: true, + scrollPosition: .centeredHorizontally + ) + + underLineController.animateIndicatorToSelectedItem() + } } // MARK: - Bind @@ -146,7 +170,7 @@ public extension DictionaryMainViewController { .flatMapLatest { _ in reactor.pulse(\.$route) } .observe(on: MainScheduler.instance) .withUnretained(self) - .subscribe { (owner, route) in + .subscribe { owner, route in switch route { case .search: let controller = owner.searchFactory.make() @@ -162,6 +186,22 @@ public extension DictionaryMainViewController { } } .disposed(by: disposeBag) + + reactor.state + .map(\.currentPageIndex) + .distinctUntilChanged() + .skip(1) + .withUnretained(self) + .subscribe(onNext: { owner, newIndex in + owner.moveToTab(index: newIndex) + }) + .disposed(by: disposeBag) + } +} + +public extension DictionaryMainViewController { + func changeTab(index: Int) { + reactor?.action.onNext(.changeTab(index)) } } @@ -182,9 +222,7 @@ extension DictionaryMainViewController: UIPageViewControllerDataSource, UIPageVi public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed, let visibleViewController = pageViewController.viewControllers?.first, let newIndex = viewControllers.firstIndex(of: visibleViewController) { - currentPageIndex.accept(newIndex) - mainView.tabCollectionView.selectItem(at: IndexPath(item: newIndex, section: 0), animated: true, scrollPosition: .centeredHorizontally) - underLineController.animateIndicatorToSelectedItem() + reactor?.action.onNext(.changeTab(newIndex)) } } } @@ -203,26 +241,15 @@ extension DictionaryMainViewController: UICollectionViewDataSource, UICollection } let title = reactor.currentState.sections[indexPath.row] cell.inject(title: title) - cell.isSelected = indexPath.row == currentPageIndex.value + cell.isSelected = indexPath.row == reactor.currentState.currentPageIndex return cell } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let newIndex = indexPath.row - let oldIndex = currentPageIndex.value - + guard let oldIndex = reactor?.currentState.currentPageIndex else { return } guard newIndex != oldIndex else { return } - let direction: UIPageViewController.NavigationDirection = newIndex > oldIndex ? .forward : .reverse - - mainView.pageViewController.setViewControllers( - [viewControllers[newIndex]], - direction: direction, - animated: true, - completion: nil - ) - - currentPageIndex.accept(newIndex) - underLineController.animateIndicatorToSelectedItem() + reactor?.action.onNext(.changeTab(newIndex)) } } From 9f89d9feacd5c0086a4f12440611745a7eaab4f1 Mon Sep 17 00:00:00 2001 From: p2glet Date: Wed, 10 Dec 2025 00:14:47 +0900 Subject: [PATCH 13/34] =?UTF-8?q?feat/#273:=20=EC=BB=AC=EB=A0=89=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EB=B6=81=EB=A7=88=ED=81=AC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=ED=9B=84=20=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionEdit/CollectionEditReactor.swift | 1 - .../CollectionEdit/CollectionEditViewController.swift | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditReactor.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditReactor.swift index 924a8665..56e34e8e 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditReactor.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditReactor.swift @@ -47,7 +47,6 @@ public final class CollectionEditReactor: Reactor { case .backButtonTapped: return .just(.navigateTo(.dismiss)) case .addCollectionButtonTapped: - // 체크한 북마크를 selectedCollections에 추가하고 route를 .collectionList로 return .just(.navigateTo(.collcectionList)) case .completeButtonTapped: // 선택된 북마크들을 선택된 컬렉션들에 저장 diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditViewController.swift index 74d2f552..bc90b230 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditViewController.swift @@ -127,7 +127,11 @@ extension CollectionEditViewController { case .dismiss: owner.navigationController?.popViewController(animated: true) case .collcectionList: - let viewController = owner.bookmarkModalFactory.make(bookmarkIds: reactor.currentState.selectedItems.map { $0.bookmarkId }) + let viewController = owner.bookmarkModalFactory.make(bookmarkIds: reactor.currentState.selectedItems.map { $0.bookmarkId }) { isSave in + if isSave { + owner.navigationController?.popToRootViewController(animated: true) + } + } owner.present(viewController, animated: true) default: break From 610cb538e41cc591f67a8d4fb94dbb37d00e77e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Dec 2025 15:20:51 +0000 Subject: [PATCH 14/34] style/#273: Apply SwiftLint autocorrect --- .../OnBoardingNotificationViewController.swift | 2 +- .../OnBoardingNotificationSheetViewController.swift | 2 +- .../BaseFeature/BaseFeature/Interface/EmptyViewState.swift | 4 ++-- .../BaseFeature/Utills/NotificationPermissionManager.swift | 2 +- .../DictionaryDetail/DictionaryDetailBaseViewController.swift | 2 +- .../DictionaryNotificationViewController.swift | 2 +- .../MonsterFilterBottomSheetViewController.swift | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift index 068cdff0..08566c23 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift @@ -76,7 +76,7 @@ public extension OnBoardingNotificationViewController { .map { Reactor.Action.nextButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.headerView.leftButton.rx.tap .map { Reactor.Action.backButtonTapped } .bind(to: reactor.action) diff --git a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift index c67b31ca..ba30890a 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift @@ -67,7 +67,7 @@ extension OnBoardingNotificationSheetViewController { .map { _ in Reactor.Action.viewWillAppear } .bind(to: reactor.action) .disposed(by: disposeBag) - + NotificationCenter.default.rx.notification(UIApplication.willEnterForegroundNotification) .map { _ in Reactor.Action.appWillEnterForeground } .bind(to: reactor.action) diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift b/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift index 7df0bbd3..607bcbc1 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift @@ -1,5 +1,5 @@ // -//public enum ViewType: Equatable { +// public enum ViewType: Equatable { // case dictionary(ViewState) // case bookmark(ViewState) // @@ -54,4 +54,4 @@ // return nil // } // } -//} +// } diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift index a437f5b6..6665635e 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift @@ -16,7 +16,7 @@ public final class NotificationPermissionManager { public func requestIfNeeded( application: UIApplication = .shared, completion: ((Bool) -> Void)? = nil - ) -> Void { + ) { let center = UNUserNotificationCenter.current() center.getNotificationSettings { settings in switch settings.authorizationStatus { diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index 2df83c67..99a66b0d 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -280,7 +280,7 @@ extension DictionaryDetailBaseViewController { ) return } - + let itemId = id(item) if isBookmarked(item) { diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift index baa81a69..281e796a 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift @@ -107,7 +107,7 @@ public extension DictionaryNotificationViewController { case .dismiss: owner.navigationController?.popViewController(animated: true) case .setting: - guard let reactor = owner.reactor , + guard let reactor = owner.reactor, let profile = reactor.currentState.profile else { return } let viewController = owner.notificationSettingFactory.make(isAgreeEventNotification: profile.eventAgreement, isAgreeNoticeNotification: profile.noticeAgreement, isAgreePatchNoteNotification: profile.patchNoteAgreement) owner.navigationController?.pushViewController(viewController, animated: true) diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetViewController.swift index 2a038744..b94640af 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/MonsterFilterBottomSheet/MonsterFilterBottomSheetViewController.swift @@ -71,7 +71,7 @@ extension MonsterFilterBottomSheetViewController { .map { Reactor.Action.cancelButtonTapped } .bind(to: reactor.action) .disposed(by: disposeBag) - + mainView.clearButton.rx.tap .map { Reactor.Action.clearButtonTapped } .bind(to: reactor.action) From 2f90c3830d7929c30b744c79d7265fb00e21eda8 Mon Sep 17 00:00:00 2001 From: p2glet Date: Wed, 10 Dec 2025 16:24:50 +0900 Subject: [PATCH 15/34] =?UTF-8?q?fix/#273:=20=EB=B9=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B6=81=EB=A7=88=ED=81=AC=20UI=20=ED=83=AD=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 --- .../Utills/TabBarUnderlineController.swift | 19 +++++++++++++++++-- .../BookmarkMainViewController.swift | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Utills/TabBarUnderlineController.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/TabBarUnderlineController.swift index 1f36cc36..e6ff6130 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/Utills/TabBarUnderlineController.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/Utills/TabBarUnderlineController.swift @@ -33,7 +33,7 @@ public final class TabBarUnderlineController { // MARK: - Initialization - public init() { } + public init() {} @available(*, unavailable) required init?(coder: NSCoder) { @@ -66,7 +66,6 @@ private extension TabBarUnderlineController { // MARK: - Public Interface public extension TabBarUnderlineController { - /// 컬렉션 뷰에 인디케이터 컨트롤러 연결 func configure(with collectionView: UICollectionView) { self.collectionView = collectionView @@ -126,4 +125,20 @@ public extension TabBarUnderlineController { ) selectionIndicatorView.frame = targetFrame } + + func setHidden(hidden: Bool, animated: Bool = false) { + let alpha: CGFloat = hidden ? 0 : 1 + if animated { + UIView.animate(withDuration: 0.25) { + self.selectionIndicatorView.alpha = alpha + self.bottomUnderlineView.alpha = alpha + } + } else { + selectionIndicatorView.alpha = alpha + bottomUnderlineView.alpha = alpha + } + + selectionIndicatorView.isHidden = hidden + bottomUnderlineView.isHidden = hidden + } } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift index efca0833..8c36fc05 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift @@ -187,6 +187,7 @@ public extension BookmarkMainViewController { .observe(on: MainScheduler.instance) .bind { owner, isLogin in owner.mainView.updateLoginState(isLogin: isLogin) + owner.underLineController.setHidden(hidden: true) } .disposed(by: disposeBag) From bab77a81a1f8a25f2d5fe756eaf266e2df026352 Mon Sep 17 00:00:00 2001 From: p2glet Date: Wed, 10 Dec 2025 17:00:18 +0900 Subject: [PATCH 16/34] =?UTF-8?q?fix/#273:=20=ED=83=AD=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=9D=B4=EB=8F=99=20=EB=B0=A9=ED=96=A5=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 --- .../DictionaryMain/DictionaryMainReactor.swift | 11 +++++++---- .../DictionaryMainViewController.swift | 16 +++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift index 02730ebb..ca3224a5 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift @@ -21,7 +21,7 @@ public final class DictionaryMainReactor: Reactor { public enum Mutation { case navigateTo(Route) case setLogin(Bool) - case setCurrentTab(Int) + case setCurrentTab(oldIndex: Int, newIndex: Int) } public struct State { @@ -32,6 +32,7 @@ public final class DictionaryMainReactor: Reactor { } var isLogin = false var currentPageIndex = 0 + var oldPageIndex = 0 } // MARK: - properties @@ -57,7 +58,8 @@ public final class DictionaryMainReactor: Reactor { case .notificationButtonTapped: return .just(.navigateTo(currentState.isLogin ? .notification : .login)) case let .changeTab(index): - return .just(.setCurrentTab(index)) + let oldIndex = currentState.currentPageIndex + return .just(.setCurrentTab(oldIndex: oldIndex, newIndex: index)) } } @@ -69,8 +71,9 @@ public final class DictionaryMainReactor: Reactor { newState.route = route case let .setLogin(isLogin): newState.isLogin = isLogin - case let .setCurrentTab(index): - newState.currentPageIndex = index + case let .setCurrentTab(oldIndex, newIndex): + newState.oldPageIndex = oldIndex + newState.currentPageIndex = newIndex } return newState diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift index b3eaa691..c4c1a01c 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainViewController.swift @@ -116,22 +116,19 @@ private extension DictionaryMainViewController { } } - func moveToTab(index: Int) { - guard index < viewControllers.count, - let reactor = reactor else { return } - - let oldIndex = reactor.currentState.currentPageIndex - let direction: UIPageViewController.NavigationDirection = index > oldIndex ? .forward : .reverse + func moveToTab(oldIndex: Int, newIndex: Int) { + guard newIndex < viewControllers.count else { return } + let direction: UIPageViewController.NavigationDirection = newIndex > oldIndex ? .forward : .reverse mainView.pageViewController.setViewControllers( - [viewControllers[index]], + [viewControllers[newIndex]], direction: direction, animated: true, completion: nil ) mainView.tabCollectionView.selectItem( - at: IndexPath(item: index, section: 0), + at: IndexPath(item: newIndex, section: 0), animated: true, scrollPosition: .centeredHorizontally ) @@ -193,7 +190,8 @@ public extension DictionaryMainViewController { .skip(1) .withUnretained(self) .subscribe(onNext: { owner, newIndex in - owner.moveToTab(index: newIndex) + let oldIndex = reactor.currentState.oldPageIndex + owner.moveToTab(oldIndex: oldIndex, newIndex: newIndex) }) .disposed(by: disposeBag) } From 856c4d8252644a07cd643fe5a36e03622ed4ab07 Mon Sep 17 00:00:00 2001 From: p2glet Date: Wed, 10 Dec 2025 17:55:49 +0900 Subject: [PATCH 17/34] =?UTF-8?q?fix/#273:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/CheckNickNameUseCaseImpl.swift | 10 ++++++- .../SetProfile/SetProfileReactor.swift | 27 +++++++++++-------- .../SetProfile/SetProfileView.swift | 2 +- .../SetProfile/SetProfileViewController.swift | 13 +++++++-- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/MLS/Domain/Domain/UseCaseImpl/Shared/CheckNickNameUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/Shared/CheckNickNameUseCaseImpl.swift index eb31680d..921d295f 100644 --- a/MLS/Domain/Domain/UseCaseImpl/Shared/CheckNickNameUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/Shared/CheckNickNameUseCaseImpl.swift @@ -1,3 +1,5 @@ +import Foundation + import DomainInterface import RxSwift @@ -6,6 +8,12 @@ public class CheckNickNameUseCaseImpl: CheckNickNameUseCase { public init() {} public func execute(nickName: String) -> Observable { - return .just((nickName).contains("병")) + let pattern = "^[가-힣ㄱ-ㅎㅏ-ㅣ]{2,15}$" + + let trimmed = nickName.trimmingCharacters(in: .whitespacesAndNewlines) + let isValid = NSPredicate(format: "SELF MATCHES %@", pattern) + .evaluate(with: trimmed) + + return .just(isValid) } } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileReactor.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileReactor.swift index 25141721..da27d604 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileReactor.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileReactor.swift @@ -46,6 +46,7 @@ public final class SetProfileReactor: Reactor { var isShowError = false var isEditingNickName = false var profile: MyPageResponse? + var nickName = "" } // MARK: - Properties @@ -74,7 +75,7 @@ public final class SetProfileReactor: Reactor { case .inputNickName(let nickName): return checkNickNameUseCase.execute(nickName: nickName) .map { isValid in - [.setNickName(nickName), .showError(isValid)] + [.setNickName(nickName), .showError(!isValid)] } .flatMap { Observable.from($0) } case .beginEditingNickName: @@ -89,14 +90,17 @@ public final class SetProfileReactor: Reactor { case .editButtonTapped: switch currentState.setProfileState { case .edit: - guard let profile = currentState.profile else { return .empty() } - return updateNickNameUseCase.execute(nickName: profile.nickname) - .flatMap { profile in - Observable.concat([ - .just(.setProfile(profile)), - .just(.completeEditting) - ]) - } + if currentState.isShowError { + return .empty() + } else { + return updateNickNameUseCase.execute(nickName: currentState.nickName) + .flatMap { profile in + Observable.concat([ + .just(.setProfile(profile)), + .just(.completeEditting) + ]) + } + } case .normal: return .just(.beginEditting) } @@ -135,8 +139,9 @@ public final class SetProfileReactor: Reactor { newState.route = .dismissWithUpdate case .setProfile(let profile): newState.profile = profile - case .setNickName(let nickName): - newState.profile?.nickname = nickName + newState.nickName = profile?.nickname ?? "" + case .setNickName(let nickname): + newState.nickName = nickname } return newState diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileView.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileView.swift index 42ed9c89..95503d80 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileView.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileView.swift @@ -229,7 +229,7 @@ public final class SetProfileView: UIView { return view }() - private let errorMessage = ErrorMessage(message: "비속어 사용은 불가능해요!") + private let errorMessage = ErrorMessage(message: "닉네임은 15자 이하로 입력해주세요.") private let countLabel = UILabel() diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift index bc29b5ac..9e472b43 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift @@ -136,15 +136,24 @@ extension SetProfileViewController { .withUnretained(self) .observe(on: MainScheduler.instance) .bind(onNext: { owner, profile in - owner.mainView.setName(name: profile.nickname) owner.mainView.setImage(imageUrl: profile.profileUrl) owner.mainView.setPlatform(platform: profile.platform) }) .disposed(by: disposeBag) + + reactor.state + .compactMap(\.nickName) + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind(onNext: { owner, nickname in + owner.mainView.setName(name: nickname) + }) + .disposed(by: disposeBag) reactor.state .filter(\.isEditingNickName) - .compactMap(\.profile?.nickname) + .compactMap(\.nickName) .distinctUntilChanged() .withUnretained(self) .observe(on: MainScheduler.instance) From 10f1449ebbeecaf8645e9d56d622f281adb42cc0 Mon Sep 17 00:00:00 2001 From: p2glet Date: Wed, 10 Dec 2025 18:05:47 +0900 Subject: [PATCH 18/34] =?UTF-8?q?fix/#273:=20=EB=8B=89=EB=84=A4=EC=9E=84?= =?UTF-8?q?=20textField=20clearButtonMode=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystem/DesignSystem/Components/InputBox.swift | 9 +++++++-- .../SetProfile/SetProfileViewController.swift | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Components/InputBox.swift b/MLS/Presentation/DesignSystem/DesignSystem/Components/InputBox.swift index 07049874..9ee776a5 100644 --- a/MLS/Presentation/DesignSystem/DesignSystem/Components/InputBox.swift +++ b/MLS/Presentation/DesignSystem/DesignSystem/Components/InputBox.swift @@ -12,7 +12,11 @@ public final class InputBox: UIStackView { // MARK: - Components public let label = UILabel() - public let textField = UITextField() + public let textField: UITextField = { + let textField = UITextField() + textField.clearButtonMode = .whileEditing + return textField + }() public lazy var borderView: UIView = { let view = UIView() @@ -24,7 +28,8 @@ public final class InputBox: UIStackView { textField.snp.makeConstraints { make in make.verticalEdges.equalToSuperview().inset(16) - make.horizontalEdges.equalToSuperview().inset(20) + make.leading.equalToSuperview().inset(20) + make.trailing.equalToSuperview().inset(10) } return view }() diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift index 9e472b43..5edac989 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/SetProfile/SetProfileViewController.swift @@ -140,7 +140,7 @@ extension SetProfileViewController { owner.mainView.setPlatform(platform: profile.platform) }) .disposed(by: disposeBag) - + reactor.state .compactMap(\.nickName) .distinctUntilChanged() From 2a96d14bba6f365d324dca5475466209cf58de17 Mon Sep 17 00:00:00 2001 From: p2glet Date: Wed, 10 Dec 2025 18:16:15 +0900 Subject: [PATCH 19/34] =?UTF-8?q?fix/#273:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=8E=B8=EC=A7=91=20=EC=95=84=EC=9D=B4=EC=BD=98=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 --- .../CollectionViewCells/DictionaryListCell.swift | 2 +- .../CollectionEdit/CollectionEditViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift b/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift index a060c4d4..1909dfd9 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift @@ -78,7 +78,7 @@ public extension DictionaryListCell { if let url = URL(string: input.imageUrl) { ImageLoader.shared.loadImage(url: url) { [weak self] image in guard let self = self else { return } - // ⚠️ 셀이 재사용된 경우, indexPath가 다르면 무시 + // 셀이 재사용된 경우, indexPath가 다르면 무시 if let currentIndex = collectionView.indexPath(for: self), currentIndex == indexPath { if isMap { diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditViewController.swift index bc90b230..add5f4c0 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditViewController.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/CollectionEdit/CollectionEditViewController.swift @@ -163,7 +163,7 @@ extension CollectionEditViewController: UICollectionViewDelegate, UICollectionVi } cell.inject( - type: .bookmark, + type: .checkbox, input: DictionaryListCell.Input( type: item.type, mainText: item.name, From 32e34a9a6bc3cf7c9f0295f75d4477d95c0b49e0 Mon Sep 17 00:00:00 2001 From: p2glet Date: Wed, 10 Dec 2025 21:01:50 +0900 Subject: [PATCH 20/34] =?UTF-8?q?fix/#273:=20=EB=A0=88=EB=B2=A8=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=B4=88=EA=B8=B0=EA=B0=92=20=EC=88=98?= =?UTF-8?q?=EC=A0=95(0=20->=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DictionaryMainReactor.swift | 1 + ...DictionaryNotificationViewController.swift | 31 +++++++++++++++++-- .../ItemFilterBottomSheetReactor.swift | 2 +- .../Views/FilterLevelSectionView.swift | 14 +++------ 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift index ca3224a5..49a8a7fa 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift @@ -53,6 +53,7 @@ public final class DictionaryMainReactor: Reactor { case .viewWillAppear: return fetchProfileUseCase.execute() .map { .setLogin($0 != nil) } + .catchAndReturn(.setLogin(false)) case .searchButtonTapped: return .just(.navigateTo(.search)) case .notificationButtonTapped: diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift index baa81a69..679768bd 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift @@ -14,6 +14,8 @@ public final class DictionaryNotificationViewController: BaseViewController, Vie // MARK: - Properties public var disposeBag = DisposeBag() + + private var lastPagingTime: Date = .distantPast private var notificationSettingFactory: NotificationSettingFactory @@ -57,7 +59,6 @@ private extension DictionaryNotificationViewController { func configureUI() { isBottomTabbarHidden = true guard let reactor = reactor else { return } - mainView.setEmpty(isEmpty: reactor.currentState.profile?.noticeAgreement == false) mainView.notificationCollectionView.delegate = self mainView.notificationCollectionView.dataSource = self @@ -107,7 +108,7 @@ public extension DictionaryNotificationViewController { case .dismiss: owner.navigationController?.popViewController(animated: true) case .setting: - guard let reactor = owner.reactor , + guard let reactor = owner.reactor, let profile = reactor.currentState.profile else { return } let viewController = owner.notificationSettingFactory.make(isAgreeEventNotification: profile.eventAgreement, isAgreeNoticeNotification: profile.noticeAgreement, isAgreePatchNoteNotification: profile.patchNoteAgreement) owner.navigationController?.pushViewController(viewController, animated: true) @@ -125,6 +126,17 @@ public extension DictionaryNotificationViewController { owner.mainView.notificationCollectionView.reloadData() } .disposed(by: disposeBag) + + reactor.state + .compactMap { $0.profile } + .distinctUntilChanged() + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, profile in + let isEmpty = profile.noticeAgreement == false && profile.eventAgreement == false && profile.patchNoteAgreement == false + owner.mainView.setEmpty(isEmpty: isEmpty) + } + .disposed(by: disposeBag) } } @@ -142,4 +154,19 @@ extension DictionaryNotificationViewController: UICollectionViewDelegate, UIColl cell.inject(input: DictionaryNotificationCell.Input(title: item.title, subTitle: item.date.changeKoreanDate(), isChecked: item.alreadyRead)) return cell } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + let now = Date() + guard now.timeIntervalSince(lastPagingTime) > 0.5 else { return } + + guard let reactor = reactor else { return } + + let offsetY = scrollView.contentOffset.y + let contentHeight = scrollView.contentSize.height + let frameHeight = scrollView.frame.size.height + + if offsetY > contentHeight - frameHeight - 100 { + reactor.action.onNext(.loadMore) + } + } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/ItemFilterBottomSheetReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/ItemFilterBottomSheetReactor.swift index 11d7ba3a..4a2e3099 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/ItemFilterBottomSheetReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/ItemFilterBottomSheetReactor.swift @@ -47,7 +47,7 @@ public final class ItemFilterBottomSheetReactor: Reactor { var etcItems: [String] = ["마스터리북", "스킬북", "소비", "설치", "이동수단"] var selectedScrollIndexes: Int? var selectedItemIndexes: [IndexPath] = [] - var levelRange: (low: Int, high: Int) = (0, 200) + var levelRange: (low: Int, high: Int) = (1, 200) @Pulse var route: Route = .none } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterLevelSectionView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterLevelSectionView.swift index 2b158d67..8a07698d 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterLevelSectionView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/ItemFilterBottomSheet/Views/FilterLevelSectionView.swift @@ -127,15 +127,15 @@ private extension FilterLevelSectionView { } } } -public extension FilterLevelSectionView { +public extension FilterLevelSectionView { func bind() { slider.lowerValueObservable .withUnretained(self) .subscribe { owner, value in guard !owner.isEdit else { return } let lowValue = Int(value) - owner.leftInputBox.textField.text = lowValue == 1 ? nil : "\(lowValue)" + owner.leftInputBox.textField.text = value == owner.slider.minimumValue ? "1" : "\(lowValue)" } .disposed(by: disposeBag) @@ -144,7 +144,7 @@ public extension FilterLevelSectionView { .subscribe { owner, value in guard !owner.isEdit else { return } let upperValue = Int(value) - owner.rightInputBox.textField.text = upperValue == 200 ? nil : "\(upperValue)" + owner.rightInputBox.textField.text = value == owner.slider.minimumValue ? "200" : "\(upperValue)" } .disposed(by: disposeBag) @@ -152,11 +152,9 @@ public extension FilterLevelSectionView { .debounce(.milliseconds(100), scheduler: MainScheduler.instance) .withUnretained(self) .subscribe { owner, text in - guard !owner.isEdit else { return } + guard !owner.isEdit, !text.isEmpty else { return } if let value = Double(text) { owner.slider.lowerValue = value - } else { - owner.slider.lowerValue = 1 } } .disposed(by: disposeBag) @@ -165,11 +163,9 @@ public extension FilterLevelSectionView { .debounce(.milliseconds(100), scheduler: MainScheduler.instance) .withUnretained(self) .subscribe { owner, text in - guard !owner.isEdit else { return } + guard !owner.isEdit, !text.isEmpty else { return } if let value = Double(text) { owner.slider.upperValue = value - } else { - owner.slider.upperValue = 200 } } .disposed(by: disposeBag) From a3e051de89a8eb7ab4b86a77af74ab0bdda9645b Mon Sep 17 00:00:00 2001 From: p2glet Date: Wed, 10 Dec 2025 22:43:22 +0900 Subject: [PATCH 21/34] =?UTF-8?q?fix/#273:=20=EC=B5=9C=EA=B7=BC=EA=B2=80?= =?UTF-8?q?=EC=83=89=EC=96=B4=ED=83=AD=20=EB=85=B8=EC=B6=9C=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LayoutFactory.swift | 4 +- .../DictionarySearchReactor.swift | 4 - .../DictionarySearchViewController.swift | 104 ++++++------------ .../DictionarySearch/EmptyRecentCell.swift | 35 ++++++ 4 files changed, 73 insertions(+), 74 deletions(-) create mode 100644 MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/EmptyRecentCell.swift diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Utills/CompositionalLayoutBuilder/LayoutFactory.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/CompositionalLayoutBuilder/LayoutFactory.swift index 12af8d7f..a79c95f7 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/Utills/CompositionalLayoutBuilder/LayoutFactory.swift +++ b/MLS/Presentation/BaseFeature/BaseFeature/Utills/CompositionalLayoutBuilder/LayoutFactory.swift @@ -65,12 +65,12 @@ public class LayoutFactory { .contentInsets(.init(top: 5, leading: 0, bottom: 5, trailing: 0)) } - public func getPopularResultLayout(hasRecent: Bool) -> CompositionalSectionBuilder { + public func getPopularResultLayout() -> CompositionalSectionBuilder { return CompositionalSectionBuilder() .item(width: .fractionalWidth(1.0), height: .estimated(40)) .group(.horizontal, width: .fractionalWidth(1.0), height: .estimated(40), count: 2) .buildSection() - .header(height: hasRecent ? 44 : 25) + .header(height: 44) .contentInsets(.init(top: 16, leading: 16, bottom: 16, trailing: 16)) } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchReactor.swift index 5f7cb884..cb2819dd 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchReactor.swift @@ -33,10 +33,6 @@ public final class DictionarySearchReactor: Reactor { public struct State { @Pulse var route: Route var recentResult: [String] - var hasRecent: Bool { - !recentResult.isEmpty - } - let popularResult: [PopularItem] } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchViewController.swift index f03f75dc..49dbaf8f 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/DictionarySearchViewController.swift @@ -63,6 +63,7 @@ private extension DictionarySearchViewController { mainView.searchCollectionView.collectionViewLayout = createLayout() mainView.searchCollectionView.delegate = self mainView.searchCollectionView.dataSource = self + mainView.searchCollectionView.register(EmptyRecentCell.self, forCellWithReuseIdentifier: EmptyRecentCell.identifier) mainView.searchCollectionView.register(TagChipCell.self, forCellWithReuseIdentifier: TagChipCell.identifier) mainView.searchCollectionView.register(PopularResultCell.self, forCellWithReuseIdentifier: PopularResultCell.identifier) mainView.searchCollectionView.register( @@ -81,9 +82,7 @@ private extension DictionarySearchViewController { let layoutFactory = LayoutFactory() let layout = UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in - guard let self = self, - let reactor = self.reactor - else { + guard self != nil else { return NSCollectionLayoutSection( group: NSCollectionLayoutGroup.vertical( layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(1)), @@ -92,24 +91,15 @@ private extension DictionarySearchViewController { ) } - if reactor.currentState.hasRecent { - switch sectionIndex { - case 0: - return layoutFactory.getTagChipLayout().build() - case 1: - return layoutFactory.getDecorationSection().build() - case 2: - return layoutFactory.getPopularResultLayout(hasRecent: true).build() - default: - return nil - } - } else { - switch sectionIndex { - case 0: - return layoutFactory.getPopularResultLayout(hasRecent: false).build() - default: - return nil - } + switch sectionIndex { + case 0: + return layoutFactory.getTagChipLayout().build() + case 1: + return layoutFactory.getDecorationSection().build() + case 2: + return layoutFactory.getPopularResultLayout().build() + default: + return nil } } @@ -157,7 +147,6 @@ extension DictionarySearchViewController { func bindViewState(reactor: Reactor) { reactor.state.map { $0.recentResult } - .distinctUntilChanged() .withUnretained(self) .observe(on: MainScheduler.instance) .subscribe(onNext: { owner, _ in @@ -192,28 +181,20 @@ extension DictionarySearchViewController { // MARK: - Delegate extension DictionarySearchViewController: UICollectionViewDelegate, UICollectionViewDataSource { public func numberOfSections(in collectionView: UICollectionView) -> Int { - return reactor?.currentState.hasRecent == true ? 3 : 1 + return 3 } public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { guard let reactor = reactor else { return 0 } - if reactor.currentState.hasRecent { - switch section { - case 0: - return reactor.currentState.recentResult.count - case 2: - return true ? 0 : reactor.currentState.popularResult.count // TODO: 인기검색어는 추후에 - default: - return 0 - } - } else { - switch section { - case 0: - return true ? 0 : reactor.currentState.popularResult.count // TODO: 인기검색어는 추후에 - default: - return 0 - } + switch section { + case 0: + let recentResult = reactor.currentState.recentResult + return recentResult.count == 0 ? 1 : recentResult.count + case 2: + return true ? 0 : reactor.currentState.popularResult.count // TODO: 인기검색어는 추후에 + default: + return 0 } } @@ -222,10 +203,13 @@ extension DictionarySearchViewController: UICollectionViewDelegate, UICollection let section = indexPath.section - if reactor.currentState.hasRecent { - switch section { - case 0: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TagChipCell.identifier, for: indexPath) as! TagChipCell + switch section { + case 0: + if reactor.currentState.recentResult.isEmpty { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: EmptyRecentCell.identifier, for: indexPath) as? EmptyRecentCell else { return UICollectionViewCell() } + return cell + } else { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TagChipCell.identifier, for: indexPath) as? TagChipCell else { return UICollectionViewCell() } let item = reactor.currentState.recentResult[indexPath.row] cell.inject(title: item, style: .search) @@ -240,27 +224,15 @@ extension DictionarySearchViewController: UICollectionViewDelegate, UICollection .disposed(by: cell.disposeBag) return cell - - case 2: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PopularResultCell.identifier, for: indexPath) as! PopularResultCell - let item = reactor.currentState.popularResult[indexPath.item] - cell.inject(input: .init(text: item.name, rank: item.rank)) - return cell - - default: - return UICollectionViewCell() } - } else { - switch section { - case 0: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PopularResultCell.identifier, for: indexPath) as! PopularResultCell - let item = reactor.currentState.popularResult[indexPath.item] - cell.inject(input: .init(text: item.name, rank: item.rank)) - return cell + case 2: + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PopularResultCell.identifier, for: indexPath) as! PopularResultCell + let item = reactor.currentState.popularResult[indexPath.item] + cell.inject(input: .init(text: item.name, rank: item.rank)) + return cell - default: - return UICollectionViewCell() - } + default: + return UICollectionViewCell() } } @@ -269,10 +241,8 @@ extension DictionarySearchViewController: UICollectionViewDelegate, UICollection viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath ) -> UICollectionReusableView { - guard let reactor = reactor else { return UICollectionReusableView() } - switch indexPath.section { - case 0 where reactor.currentState.hasRecent: + case 0: let view = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: RecentSearchHeaderView.identifier, @@ -280,16 +250,14 @@ extension DictionarySearchViewController: UICollectionViewDelegate, UICollection ) as! RecentSearchHeaderView return view - case reactor.currentState.hasRecent ? 2 : 0: + case 2: let view = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: PopularSearchHeaderView.identifier, for: indexPath ) as! PopularSearchHeaderView // TODO: 인기검색어 추후에 - // view.inject(mainText: "인기 검색어", subText: "업데이트 일자", hasRecent: reactor.currentState.hasRecent) return view - default: return UICollectionReusableView() } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/EmptyRecentCell.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/EmptyRecentCell.swift new file mode 100644 index 00000000..2a2f418d --- /dev/null +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/EmptyRecentCell.swift @@ -0,0 +1,35 @@ +import UIKit + +import SnapKit + +final class EmptyRecentCell: UICollectionViewCell { + private let label: UILabel = { + let label = UILabel() + label.attributedText = .makeStyledString(font: .b_s_r, text: "최근 검색어 내역이 없습니다", color: .neutral600) + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addViews() + setUpConstraints() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } +} + +// MARK: SetUp +private extension EmptyRecentCell { + func addViews() { + contentView.addSubview(label) + } + + func setUpConstraints() { + label.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.height.equalTo(32) + } + + } +} From 6a16ec4eb2fbbeffad654b4b43ca23833619ac28 Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 11 Dec 2025 00:15:24 +0900 Subject: [PATCH 22/34] =?UTF-8?q?fix/#273:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EC=98=A8=EB=B3=B4=EB=94=A9=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserDefaultsRepositoryImpl.swift | 18 +++++++++++++ .../FetchVisitBookmarkUseCaseImpl.swift | 23 +++++++++++++++++ .../RecentSearchRemoveUseCaseImpl.swift | 1 - .../Repository/UserDefaultsRepository.swift | 3 +++ .../Bookmark/FetchVisitBookmarkUseCase.swift | 5 ++++ MLS/MLS/Application/AppDelegate.swift | 16 +++++------- .../BookmarkMainFactoryImpl.swift | 8 +++--- .../BookmarkMain/BookmarkMainReactor.swift | 24 ++++++++++++------ .../BookmarkOnBoardingView.swift | 4 +-- .../Contents.json | 0 .../bookmarkList.png | Bin 11 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 MLS/Domain/Domain/UseCaseImpl/Bookmark/FetchVisitBookmarkUseCaseImpl.swift create mode 100644 MLS/Domain/DomainInterface/UseCase/Bookmark/FetchVisitBookmarkUseCase.swift rename MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/{bookmarkList.imageset => onBoardingBookmark.imageset}/Contents.json (100%) rename MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/{bookmarkList.imageset => onBoardingBookmark.imageset}/bookmarkList.png (100%) diff --git a/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift index f0e8f118..fe5279ae 100644 --- a/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift @@ -6,6 +6,7 @@ import RxSwift public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { private let recentSearchkey = "recentSearch" private let platformKey = "platformKey" + private let bookmarkkey = "bookmark" public init() {} @@ -67,4 +68,21 @@ public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { return Disposables.create() } } + + public func fetchBookmark() -> Observable { + return Observable.create { observer in + let hasVisited = UserDefaults.standard.bool(forKey: self.bookmarkkey) + observer.onNext(hasVisited) + observer.onCompleted() + return Disposables.create() + } + } + + public func saveBookmark() -> Completable { + return Completable.create { completable in + UserDefaults.standard.set(true, forKey: self.bookmarkkey) + completable(.completed) + return Disposables.create() + } + } } diff --git a/MLS/Domain/Domain/UseCaseImpl/Bookmark/FetchVisitBookmarkUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/Bookmark/FetchVisitBookmarkUseCaseImpl.swift new file mode 100644 index 00000000..1424d1b5 --- /dev/null +++ b/MLS/Domain/Domain/UseCaseImpl/Bookmark/FetchVisitBookmarkUseCaseImpl.swift @@ -0,0 +1,23 @@ +import DomainInterface +import Foundation + +import RxSwift + +public class FetchVisitBookmarkUseCaseImpl: FetchVisitBookmarkUseCase { + var repository: UserDefaultsRepository + public init(repository: UserDefaultsRepository) { + self.repository = repository + } + + public func execute() -> Observable { + return repository.fetchBookmark() + .flatMap { hasVisited -> Observable in + if hasVisited { + return .just(true) + } else { + return self.repository.saveBookmark() + .andThen(.just(false)) + } + } + } +} diff --git a/MLS/Domain/Domain/UseCaseImpl/RecentSearch/RecentSearchRemoveUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/RecentSearch/RecentSearchRemoveUseCaseImpl.swift index b25fad1b..9fdb545c 100644 --- a/MLS/Domain/Domain/UseCaseImpl/RecentSearch/RecentSearchRemoveUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/RecentSearch/RecentSearchRemoveUseCaseImpl.swift @@ -4,7 +4,6 @@ import Foundation import RxSwift public class RecentSearchRemoveUseCaseImpl: RecentSearchRemoveUseCase { - private let key = "recentSearch" var repository: UserDefaultsRepository public init(repository: UserDefaultsRepository) { self.repository = repository diff --git a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift index b469b8c1..af039ac1 100644 --- a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift +++ b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift @@ -7,4 +7,7 @@ public protocol UserDefaultsRepository { func fetchPlatform() -> Observable func savePlatform(platform: LoginPlatform) -> Completable + + func fetchBookmark() -> Observable + func saveBookmark() -> Completable } diff --git a/MLS/Domain/DomainInterface/UseCase/Bookmark/FetchVisitBookmarkUseCase.swift b/MLS/Domain/DomainInterface/UseCase/Bookmark/FetchVisitBookmarkUseCase.swift new file mode 100644 index 00000000..7df67704 --- /dev/null +++ b/MLS/Domain/DomainInterface/UseCase/Bookmark/FetchVisitBookmarkUseCase.swift @@ -0,0 +1,5 @@ +import RxSwift + +public protocol FetchVisitBookmarkUseCase { + func execute() -> Observable +} diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index 30d29609..1d280567 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -636,9 +636,6 @@ extension AppDelegate { ) ) } - // DIContainer.register(type: AddCollectionsToBookmarkUseCase.self) { - // AddCollectionsToBookmarkUseCaseImpl(repository: DIContainer.resolve(type: CollectionAPIRepository.self)) - // } DIContainer.register(type: SetCollectionUseCase.self) { SetCollectionUseCaseImpl( repository: DIContainer.resolve( @@ -653,9 +650,6 @@ extension AppDelegate { ) ) } - // DIContainer.register(type: AddBookmarksToCollectionUseCase.self) { - // AddBookmarksToCollectionUseCaseImpl(repository: DIContainer.resolve(type: CollectionAPIRepository.self)) - // } DIContainer.register(type: AddCollectionAndBookmarkUseCase.self) { AddCollectionAndBookmarkUseCaseImpl( repository: DIContainer.resolve( @@ -663,6 +657,9 @@ extension AppDelegate { ) ) } + DIContainer.register(type: FetchVisitBookmarkUseCase.self) { + FetchVisitBookmarkUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } } fileprivate func registerFactory() { @@ -978,9 +975,10 @@ extension AppDelegate { BookmarkMainFactoryImpl( setBookmarkUseCase: DIContainer .resolve(type: SetBookmarkUseCase.self), -// fetchProfileUseCase: DIContainer -// .resolve(type: FetchProfileUseCase.self), - checkLoginUseCase: DIContainer.resolve(type: CheckLoginUseCase.self), + checkLoginUseCase: DIContainer + .resolve(type: CheckLoginUseCase.self), + fetchVisitBookmarkUseCase: DIContainer + .resolve(type: FetchVisitBookmarkUseCase.self), onBoardingFactory: DIContainer .resolve(type: BookmarkOnBoardingFactory.self), diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift index 993a7524..8d1aaa9c 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainFactoryImpl.swift @@ -6,8 +6,8 @@ import DomainInterface public final class BookmarkMainFactoryImpl: BookmarkMainFactory { private let setBookmarkUseCase: SetBookmarkUseCase -// private let fetchProfileUseCase: FetchProfileUseCase private let checkLoginUseCase: CheckLoginUseCase + private let fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase private let onBoardingFactory: BookmarkOnBoardingFactory private let bookmarkListFactory: BookmarkListFactory @@ -18,8 +18,8 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { public init( setBookmarkUseCase: SetBookmarkUseCase, -// fetchProfileUseCase: FetchProfileUseCase, checkLoginUseCase: CheckLoginUseCase, + fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase, onBoardingFactory: BookmarkOnBoardingFactory, bookmarkListFactory: BookmarkListFactory, collectionListFactory: CollectionListFactory, @@ -28,8 +28,8 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { loginFactory: LoginFactory ) { self.setBookmarkUseCase = setBookmarkUseCase -// self.fetchProfileUseCase = fetchProfileUseCase self.checkLoginUseCase = checkLoginUseCase + self.fetchVisitBookmarkUseCase = fetchVisitBookmarkUseCase self.onBoardingFactory = onBoardingFactory self.bookmarkListFactory = bookmarkListFactory self.collectionListFactory = collectionListFactory @@ -40,7 +40,7 @@ public final class BookmarkMainFactoryImpl: BookmarkMainFactory { public func make() -> BaseViewController { let reactor = BookmarkMainReactor( - setBookmarkUseCase: setBookmarkUseCase, /*fetchProfileUseCase: fetchProfileUseCase*/checkLoginUseCase: checkLoginUseCase + setBookmarkUseCase: setBookmarkUseCase, checkLoginUseCase: checkLoginUseCase, fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase ) let viewController = BookmarkMainViewController( onBoardingFactory: onBoardingFactory, diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift index 6712a5c0..1c9d5da2 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainReactor.swift @@ -30,32 +30,40 @@ public final class BookmarkMainReactor: Reactor { var sections: [String] { return type.pageTabList.map { $0.title } } + var isLogin = false } // MARK: - Properties private let setBookmarkUseCase: SetBookmarkUseCase -// private let fetchProfileUseCase: FetchProfileUseCase private let checkLoginUseCase: CheckLoginUseCase + private let fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase public var initialState: State private let disposeBag = DisposeBag() - public init(setBookmarkUseCase: SetBookmarkUseCase, checkLoginUseCase: CheckLoginUseCase /*fetchProfileUseCase: FetchProfileUseCase*/) { + public init(setBookmarkUseCase: SetBookmarkUseCase, checkLoginUseCase: CheckLoginUseCase, fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase) { self.initialState = State(route: .none) self.setBookmarkUseCase = setBookmarkUseCase -// self.fetchProfileUseCase = fetchProfileUseCase self.checkLoginUseCase = checkLoginUseCase + self.fetchVisitBookmarkUseCase = fetchVisitBookmarkUseCase } public func mutate(action: Action) -> Observable { switch action { case .viewWillAppear: -// return fetchProfileUseCase.execute() -// .map { .setLogin($0 != nil) } - return checkLoginUseCase.execute() - .map { .setLogin($0) } + let onboardingMutation = fetchVisitBookmarkUseCase.execute() + .flatMap { hasVisited -> Observable in + if hasVisited { + return .empty() + } else { + return .just(.navigateTo(.onBoarding)) + } + } + let loginMutation = checkLoginUseCase.execute() + .map { Mutation.setLogin($0) } + return .concat([onboardingMutation, loginMutation]) case .searchButtonTapped: return Observable.just(.navigateTo(.search)) case .notificationButtonTapped: @@ -68,7 +76,7 @@ public final class BookmarkMainReactor: Reactor { public func reduce(state: State, mutation: Mutation) -> State { var newState = state switch mutation { - case .navigateTo(let route): + case let .navigateTo(route): newState.route = route case let .setLogin(isLogin): newState.isLogin = isLogin diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkOnBoarding/BookmarkOnBoardingView.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkOnBoarding/BookmarkOnBoardingView.swift index 219e47b7..44fa7c75 100644 --- a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkOnBoarding/BookmarkOnBoardingView.swift +++ b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkOnBoarding/BookmarkOnBoardingView.swift @@ -15,7 +15,7 @@ public final class BookmarkOnBoardingView: UIView { switch self { case .first: return .init( - imageName: "bookmarkList", + imageName: "onBoardingBookmark", title: "내가 찜한 정보, 한곳에!", description: "아이템, 몬스터, 맵, NPC, 퀘스트를\n북마크하면 자동으로 여기에 모여요.", isBackButtonHidden: true, @@ -151,7 +151,7 @@ public extension BookmarkOnBoardingView { guard let content = type.content else { return } imageView.image = DesignSystemAsset.image(named: content.imageName) titleLabel.attributedText = .makeStyledString(font: .h_xxxl_b, text: content.title) - descLabel.attributedText = .makeStyledString(font: .b_m_r, text: content.description) + descLabel.attributedText = .makeStyledString(font: .b_m_r, text: content.description, color: .neutral700) nextButton.updateTitle(title: content.buttonTitle) backButton.isHidden = content.isBackButtonHidden stepIndicator.selectIndicator(index: type.rawValue) diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/bookmarkList.imageset/Contents.json b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/onBoardingBookmark.imageset/Contents.json similarity index 100% rename from MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/bookmarkList.imageset/Contents.json rename to MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/onBoardingBookmark.imageset/Contents.json diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/bookmarkList.imageset/bookmarkList.png b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/onBoardingBookmark.imageset/bookmarkList.png similarity index 100% rename from MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/bookmarkList.imageset/bookmarkList.png rename to MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/image/onBoardingBookmark.imageset/bookmarkList.png From 815eb39997ca1ff46b646d0fae611a889effae7b Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 11 Dec 2025 01:53:26 +0900 Subject: [PATCH 23/34] =?UTF-8?q?feat/#273:=20=EB=94=94=ED=85=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=98=A8=EB=B3=B4=EB=94=A9=20=EA=B5=AC=ED=98=84(=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95=20=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MLS/MLS/Application/AppDelegate.swift | 57 ++++-- .../Components/CommonButton.swift | 18 +- .../BookmarkOnboarding/Contents.json | 6 + .../guideAlert.imageset/Contents.json | 21 ++ .../guideAlert.imageset/guideAlert.png | Bin 0 -> 30406 bytes .../guideArrow1.imageset/Contents.json | 21 ++ .../guideArrow1.imageset/guideArrow1.png | Bin 0 -> 1394 bytes .../guideArrow2.imageset/Contents.json | 21 ++ .../guideArrow2.imageset/guideArrow2.png | Bin 0 -> 415 bytes .../guideIcon.imageset/Contents.json | 21 ++ .../guideIcon.imageset/guideIcon.png | Bin 0 -> 2850 bytes .../Utills/Extension/UIColor+.swift | 2 +- .../DictionaryDetailBaseView.swift | 1 - .../DictionaryDetailBaseViewController.swift | 8 +- .../DictionaryDetailFactoryImpl.swift | 53 ++++- .../DetailOnBoardingFactoryImpl.swift | 14 ++ .../OnBoarding/DetailOnBoardingReactor.swift | 52 +++++ .../OnBoarding/DetailOnBoardingView.swift | 193 ++++++++++++++++++ .../DetailOnBoardingViewController.swift | 83 ++++++++ .../DetailOnBoardingFactory.swift | 5 + 20 files changed, 553 insertions(+), 23 deletions(-) create mode 100644 MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/Contents.json create mode 100644 MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideAlert.imageset/Contents.json create mode 100644 MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideAlert.imageset/guideAlert.png create mode 100644 MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/Contents.json create mode 100644 MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/guideArrow1.png create mode 100644 MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow2.imageset/Contents.json create mode 100644 MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow2.imageset/guideArrow2.png create mode 100644 MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideIcon.imageset/Contents.json create mode 100644 MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideIcon.imageset/guideIcon.png create mode 100644 MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingFactoryImpl.swift create mode 100644 MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingReactor.swift create mode 100644 MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingView.swift create mode 100644 MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingViewController.swift create mode 100644 MLS/Presentation/DictionaryFeature/DictionaryFeatureInterface/DetailOnBoardingFactory.swift diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index 1d280567..38e26204 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -18,9 +18,9 @@ import Firebase import KakaoSDKCommon import MyPageFeature import MyPageFeatureInterface -import os import UIKit import UserNotifications +import os @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -397,7 +397,8 @@ extension AppDelegate { ) ) } - DIContainer.register(type: FetchDictionaryDetailMonsterMapUseCase.self) { + DIContainer.register(type: FetchDictionaryDetailMonsterMapUseCase.self) + { FetchDictionaryDetailMonsterMapUseCaseImpl( repository: DIContainer.resolve( type: DictionaryDetailAPIRepository.self @@ -658,7 +659,11 @@ extension AppDelegate { ) } DIContainer.register(type: FetchVisitBookmarkUseCase.self) { - FetchVisitBookmarkUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + FetchVisitBookmarkUseCaseImpl( + repository: DIContainer.resolve( + type: UserDefaultsRepository.self + ) + ) } } @@ -735,6 +740,9 @@ extension AppDelegate { DIContainer .resolve(type: DictionaryDetailFactory.self) }, + detailOnBoardingFactory: DIContainer.resolve( + type: DetailOnBoardingFactory.self + ), appCoordinator: { DIContainer.resolve(type: AppCoordinatorProtocol.self) }, @@ -873,18 +881,32 @@ extension AppDelegate { } DIContainer.register(type: DictionaryNotificationFactory.self) { DictionaryNotificationFactoryImpl( - notificationSettingFactory: DIContainer.resolve(type: NotificationSettingFactory.self), - fetchAllAlarmUseCase: DIContainer.resolve(type: FetchAllAlarmUseCase.self), - fetchProfileUseCase: DIContainer.resolve(type: FetchProfileUseCase.self) + notificationSettingFactory: DIContainer.resolve( + type: NotificationSettingFactory.self + ), + fetchAllAlarmUseCase: DIContainer.resolve( + type: FetchAllAlarmUseCase.self + ), + fetchProfileUseCase: DIContainer.resolve( + type: FetchProfileUseCase.self + ) ) } DIContainer.register(type: DictionaryMainViewFactory.self) { DictionaryMainViewFactoryImpl( - dictionaryMainListFactory: DIContainer.resolve(type: DictionaryMainListFactory.self), - searchFactory: DIContainer.resolve(type: DictionarySearchFactory.self), - notificationFactory: DIContainer.resolve(type: DictionaryNotificationFactory.self), + dictionaryMainListFactory: DIContainer.resolve( + type: DictionaryMainListFactory.self + ), + searchFactory: DIContainer.resolve( + type: DictionarySearchFactory.self + ), + notificationFactory: DIContainer.resolve( + type: DictionaryNotificationFactory.self + ), loginFactory: DIContainer.resolve(type: LoginFactory.self), - fetchProfileUseCase: DIContainer.resolve(type: FetchProfileUseCase.self) + fetchProfileUseCase: DIContainer.resolve( + type: FetchProfileUseCase.self + ) ) } DIContainer.register(type: OnBoardingNotificationSheetFactory.self) { @@ -973,11 +995,14 @@ extension AppDelegate { } DIContainer.register(type: BookmarkMainFactory.self) { BookmarkMainFactoryImpl( - setBookmarkUseCase: DIContainer + setBookmarkUseCase: + DIContainer .resolve(type: SetBookmarkUseCase.self), - checkLoginUseCase: DIContainer + checkLoginUseCase: + DIContainer .resolve(type: CheckLoginUseCase.self), - fetchVisitBookmarkUseCase: DIContainer + fetchVisitBookmarkUseCase: + DIContainer .resolve(type: FetchVisitBookmarkUseCase.self), onBoardingFactory: DIContainer @@ -993,7 +1018,8 @@ extension AppDelegate { .resolve(type: DictionarySearchFactory.self), notificationFactory: DIContainer.resolve( type: DictionaryNotificationFactory.self - ), loginFactory: DIContainer.resolve(type: LoginFactory.self) + ), + loginFactory: DIContainer.resolve(type: LoginFactory.self) ) } DIContainer.register(type: BookmarkOnBoardingFactory.self) { @@ -1189,5 +1215,8 @@ extension AppDelegate { ) ) } + DIContainer.register(type: DetailOnBoardingFactory.self) { + DetailOnBoardingFactoryImpl() + } } } diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift b/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift index c2744498..ca89df0b 100644 --- a/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift +++ b/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift @@ -125,7 +125,8 @@ private extension CommonButton { config.background.backgroundColor = .clear let currentTitle = isEnabled ? title : disabledTitle if let textButtonTitle = currentTitle, - let lineHeight = style.font?.lineHeight { + let lineHeight = style.font?.lineHeight + { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.minimumLineHeight = lineHeight * Constant.textLineHeight paragraphStyle.maximumLineHeight = lineHeight * Constant.textLineHeight @@ -175,4 +176,19 @@ public extension CommonButton { } configureUI() } + + func updateTitleColor(color: UIColor, for state: UIControl.State = .normal) { + var config = configuration ?? UIButton.Configuration.plain() + + if var attributedTitle = config.attributedTitle { + var container = AttributeContainer() + container.foregroundColor = color + attributedTitle.mergeAttributes(container, mergePolicy: .keepNew) + config.attributedTitle = attributedTitle + } else if let title = title(for: state) { + config.attributedTitle = AttributedString(title, attributes: AttributeContainer([.foregroundColor: color])) + } + + configuration = config + } } diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/Contents.json b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideAlert.imageset/Contents.json b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideAlert.imageset/Contents.json new file mode 100644 index 00000000..7911bc12 --- /dev/null +++ b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideAlert.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "guideAlert.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideAlert.imageset/guideAlert.png b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideAlert.imageset/guideAlert.png new file mode 100644 index 0000000000000000000000000000000000000000..c7fa2f9f96f538a97e6ef23928bb53d341fb6c77 GIT binary patch literal 30406 zcmeFYcTkgC_b~cUMBz|8a8M~yR6vS=ARq{#c~qJpB=k_ENeR6}00qTLk={Wm5+L*- zgl3~RrGyZw5D-Fd0Rs3v!T0^$d*{xbxqsf7`(?(F?7jBdtFN{9+IeAc|1Jv?HxmE= zEL!(&8v?*70RW)$Ji`EvsOo$j2mfRAyl3VE01`UXe>4Ru$;IFxjgR474FKK8y9j=v zcT(3=2Y|}hzYc9r0f4`S)@^lYAkFfGb*a4xQsiLbf?&|kfv*7)f?Sz*G}2!Lo$+zv zd+o&@Sot_5uH!I%)$*I*Xt^`9zASs&NUt&%AqY$NIAYb3kykwKyz~)m!c45ScfxCS zr%QtMgAw+f@tU&PHTIpGDpNkeGfRWD0V}An5|_2HoLDGi#b;@#7}exzQnq$nwsUJx z`nicUa!2p3b>B71u>D_~g-^TdOH8i)W&`PNT|L@f5|We*J&G=wYB7~`>dV}B{xZ^c zqtrq0QK-HTyG--e(!U?hfS{hH>}tou!#cEGElNy6ErS*3Xy%wq=-S!%;9<3jj2EZy zAP^w!#CWB}$9KQHSL_s;A)-A!5h5R>Tz)JxYM1f(Yv4^wBPYu0$-&7r>g2bLLQJ(3a1 zOng&=nNX!k+y`HJaNb6N9a1Q>+3n+MFZ&AE9QubuzCm4Ap?W@6l8ygVytRwtbS_;- z$*0*het4e(>LLv|dm-gjZ|6TN(QH9w9g)leu^^GXTI+$+Mtx45KCaN54n*Q<1A^TI@Q{Rb%yIgo=Z z?IQdm-*KB}?!jKFOM6sodJlE9)m2hnL~t%8{coO?f0j#JRUl#8{iWoqp;TrgT(fR9 z*uy*86AI)m%XKkzlo+Aq(x^g;2$ST4Pu^-1#lJ5B`QQ&<_|2-I#rQv?-Cv=XUFsAf$pbfrMoI`93i9B34m~L@SPaYpQ*St z=-9u@r)IuJl^B5fIQrTK-qFMm_XqcfrH9oEXT3dK)%c#quDRXFI3Xsz?;^YzDff!- zstKxQg={Sj9de^OE3njU9#s*4hJ8;|B!!O|*L`p!liMosMI!;+s2bRH4^I6;i~P zgls8`{@_Wmh(@KIgXR;#-Mqb~*ah?}Sd0cTVTw;wE&W&8%Oi{)_MwLnjq}n8S-j0< zg%3TTH2X^>qI{scfN8TCE$(Zqh6aJTAf0W1zdlfQT(^;V^1$Co(0PhmU5CjD|EgXJg4 zMmBVcifpAyB0}{~uv9ct?uXl%0s9#z;kWew-F+?vTvYpK>nXp9it^$Jv2}y}VdDOn9i@B5Zlf>4Ge2f)Z8! z1Ad{e{;){*(@4b0{uh3PZ04}5{YUc8d?bI1(#(~gI1BJav5z-4W1#V1spPlsHc{^n ziPB-gpp{uG`TPSB4m>P6!D3_Dk|;X*C-_FpWO1lbJr<}vBS4iygi8$Fy?8J{rM{m|HgE|VOf!i+Njd;$>XVa&(901g zrhp1U(gCe*O`Bcfz1>nRE5tdI2$MPn*o?!X9Ou=h0?Znr$||ID8*Y`d{aQkO(I293rG{#oX_xp95Vr}wr4^1bS=d$S_jTNo&R>;nirll zTc~{@sQX%7DvAY=_M*K9M7-$QUZRgRD)F^6`P`wl2 z<}MY+j`3nn$f_bNHpG)uRwrLK?yUo=W|e9O{=Y<2_d7RgTz5r!o!M_sdVXO6wYWo- zvVUtL$?Lih4`35O9|r(Qa8QAPU49z(+Aed()lK+Y`0Os@8Gwjj>MUo|-p^Mc*=6LL zRt@%az|hVCI3<=He!M^X*Q_xn{wzC?%gRZGJi}ikGEq603C`P6_hAqY%|!cl^fiE! z%nN?t_K|->?pus3>(+S#z%!~+FFar0kUj(Q{7i`YEn}Xah(^VXF0KQTOY=9N7EV&GpR$2oQP7_n8AJ;;DG8r znePtb_3{PvBmuxu2jtT8<AGzkF0`-NnmWDf>r8fa9+5c zxU7Q$b$XwwSnk1cuI!iQKm-T4&}J8HS^v)YOr#DB0aE275Q#kpu*0xL(;?at~uYiJk0>iQ;ni0{rHa(4q$!ASbD}z?IS#X~BpC zxmtWb5K&L{zv_a6c=&@;;Hoq#%(c9;SZ`t%NT>}Ow4_!y#;2p4whG``fQl-a9b%pm z3kMl7S@xh#3eZHDu`^VkVH6q)kqi)5k5R zqb&Y|dO2V4o@9m2?ydiR{}5(xt@{OJ;vN-IjM$CcfA?5`o_H!<7X{B!E*<{Q+}Y>I z>!)~t)XUUOZfOYoKSKgKK)-< z&bXs12B90@&hwlEkogWUh1vMKH*T~s?5h7C@`62DezpET5-Qf<(5Q07uTit%A5mxt zabq2G=>DrUrhEP?g&FQHARi~vr>fD21|%?vDt#fED_CJFV}De9=f<+(t{jEuKWfE5 z)vLsJ-nRPH7G$&&!U4gZQX{z9$`cI|dyC z2|KZKJwOXVHI`K>vQnXl8!E}>RUQsZ@<3}Q)!y*f{rLk{A%pG~v)<-Yz`%`i z4*+mnO}21%rpD!GL_?v2c`(s6EG|()$;MW4xdaut0mxAA%j4 zHvC3T_bXx(3Dtex%?Q?at*%u2F%gP@ehYY*Aa{N6Vg}W(BXCq#M{Dn0y{S~GbymDl zB_4Tii|aq*=Te^`e-cxy-A8IW*LW|%KY^gsq+psG)bS$4D!AATmn3eIUT*}JxSlFB zU>Fe<8@*54;U~uJGXWM23O-K%4O-KQ&GNR1g`q<}Kvijh6aGy#?k*>Ybx;!n z)rZZ?6LN$?$L+gz1v{HB)i402t34%m!P*|Zb-2~fZ#|EOMok9JzG+YCNF<%N4y_CV z1DO(CSAEA8)rU-}!Disnr?IKuM^mI#-6Hy$x~Z@bFbR1Vf(2GhN6i?)9b5KTQVcu+ zG*CTDY20^EuNPnK#A7P_#>t!JKQa2Lg-4U$E{pTWY7V-Xg#2=<>?5HqgqI8 zi}z?3=G;v>z>VtN6@oCYtz3#C(CSZxvgk06Q?lci)B{UR4As0wfAlI$l`3}9bjjDk z!DLMxyAV;aR56+j0AHwKi23#-zYA`lQT?e8t~9306W9QoK&o*(W~?mi^rr(p%>GdV z1Nrk`Qa8XFMxVQFZN$O@;a*Qn21|<>swBl4yoo27yn?Do?zROm2;F4mAME4>B9y4a zB{dyxcDD=KH_PyO1b*R3uWMRCoa9(Aik%c>r`aKnO1s4F6rNqFJiE{BQQ)E>s=bZo zE#!nB?|nYDFKgGuf{8$ZitpC$4;?AF6iK*@P)K{#v zoK*APLrtW@gzuo&-lHx#uoR;byL*+!{>i=JIKTX*jb4wiEwCs9fL%?hM4eEAB!itZ zfHW&r1n~weim&NooMr6FKpA;a_48V{_}M?V(ISIovcpO#hdlosj&sJ7>cL15iwnLn z!424iQi;4Q{CwFl3tYNEwHe(AflYHUJ{q8qx|Osb<0TG_KCd_TT$RDO1ki+R-f5rR zr?M3-F<3hIT?rQZ1YEmFRg~_ypz*(sE`HSFRADmE4m4_CDl4oVR38Mrjo*56O;l?j zwe>n@ELfd-UZC!OT5LEjEgLRrR}!O}WXH(Ep`32T0(hoU=Vapn8CZP%mAD?9iB(Vm z{%O>kVowRM(qFOxGc_BPeJ&lq>`4cGBJu5SRiv3P>{WsH^~Vn-_kVBJM#Z3bXn=WY zeFA=#kckDc_pyER-MR$zPRj$N@N^-IDVMtg>3`;`Yn_rCDM531Razb5CabC5NU zMFYv$HJ7mM_xw^vc)_MN)y?==z80x#3FP;6jl^rcabw5E%o}xl;fF6~yJZxQ=WhC* zX^5#cI+B<{az->$9oRK^Z{}{gQw|ct^`2f{^R&Th9okQr+}daK*@*Bp%G7W45er}n zo|9hx=nvbip6!M#y9(@d>W87Y-ecFAvGohtSe|J)M*i^iNT>9A%1E7L4MSzIxdk~e zec=Y|VfUQrc1*zdWNRGzys!;im{QKg3P>~4T#?o;mOMyM4MO3gkuaZS(gynN!ov)p z3Skas$)M-MCgwWwe>&iq+H0HNB?Rz{+Z#;DBV@@!Wak{RAms(BYz6+&GjC@!Nwk0j zX=mgo#BkMLqq!ni0}E(AaQX6ePDQ>w-fggPVP0i=TWh89ccM;^R7WK-=GN*X-~(7Q ztHH3$0t$@E{-U1_AgKA>N{lHc#6o&!F2Ne#sU{U3&PwWfV1iQSs9AloV{D6LEXv@#T9vlg!Pss)G^O96GZA`YJ}7XhxG25l zh+sQ1+l!dLVypNdH36El_jNyuJ|2&D(0VmEOervzB`AD)bbor~;W?VtffJ=194@HV z)%m&1&hm%b)aZGRzIG{D)IK$d9`U*_J#386YIS1;vrofazM%Zz%Z2or^+U{;be@kn z`QJuwJ(UXG*qg%lwU9&jWf8oo|Il1%?MM<;mlL|jg?a+a=R)(ByYba1Yt}aiB?NX^ z8)C9wS`{nzd5g?`tg<~&bo#Hrtu%4LrNZv2D^a};DX?^a03ImbLr10-OG0m23iJ1k z)R$H2xoYj1h8#6#BE#1|dHK&Uq(;IU$yZwO`QE#m#9fyu83*5sSeM)1X0A?j;TVIf zW1Du$MD!fbk?VdW3fJMuyWSI&weabi%iRt<`#tFbe$T}YHun`^`{}+kc3_!t$2qH2 z+WXu`JxHMNfPLr>uh4sT`~%sl;m23cuBiqtF+DYU@xAV0ms)rr*SfV5%tqtDG{M}x~;`HfT$2WzMU)p8tu83sI%GK+`Y#50Y z`AX5jDFqfm*{4b~vvFNp2?@g*_iTFT<670FBT@_N^|;IxOxKtMWLbI=OOCYmK6bON z8Qs_x2(fyHrjf@|nBPept;R;SRf{iCs8mkL4Tkb#_UG|^S7bE%g`9g36T@Cv5=JN|MkZ;nS87G*lJjLwLgxY`dV9Ymg(*;HtepiWBZT(&r4b(ka5}^c*K~crs#rmf^#q!Mfg= z@H)}B+WZAqf}Qi^)<~K6^1IJ}pHLvm$ZE7d$|;twA;^DDme{b;<0~e(CF`nmU7lpT zE6n@L++1L$6-DD?b}!t-+dAOyF_Aqym_6d-1Vqk^&G{*gkaIKXwPxR-ar!YlipPm| z24?N$!w5SoI zd{^{{?>oH$`wV6^nGWsu^qefO)p5{XjJnr;6Mb+u!$l;wR6$qgw)%zpIpmV-&tetZ zl-FD7c=Xo#>?GGpg52y@=&e_0WgN5*vjQe-Z_H_rrgNmiyV26G(ftmHUT2p@rbGXx zWu4vbOx|DKXLP;;(O_2B)j)|ih@JcCE2mY6o%@X~Y;kB!e0+yNn_{Kok@3kqI|Ujl znb2ON_nw{KKEQ4m+DreUxYjS=NG^sHN&di=_ue^V%5r(aL5DPowU+HZ&3o)7hR4(ad4j1RkhD0wl2 z=K188^+DoU<|>Qy70@_bgc8mCFt_+^j5-4kBHg$Id|7T@;i;EhxiSpo-R#Ki3w?2kZsp>gLVux(MITrQP{%OXwF5Ch2 zdi>MR5?=m*RILygsu_b>D+dE}3QpaZVu_={qdX@M3M+0QzdUZNyCPv0s+Vln3Mq35 zO`7$H(d>@-#5lcqp+Tcoxfy;+t|+PPdI@chgVE2ek2=7Ja9u{6OOLD7eLQM$J?MDq z_c5C#xqR0_Q_Z!CoL|=tk4e&Bv0Pm!wl}ZX#{)Iep7^rst_g}_q zvcI$kf`X^z+G^`>@;V4i$``mj*!_n2bemDuxJ>T619DJi$LQmvHRgLHoBLFvorJd= zhZ^hqsQbudMlRRA3uX%yy-GTYb1M5zsuW)m_bbJDp^9SoDYIlxt(0j9r(IFeSE+=4 zX2lVIKaH3w{W-Qxa(p`U*Ky2uLM2COmHfT#v|WXvD^Aq=Lr(x723PAVEvU# zmbqOo_4%$16PU(+`Ravq*$BEaxm)zYP@lVkbsY{&87|2Y*L+#_zk(L?63&VF5?wqghP7-yv~z}XO+Za;S5b!C1=@ZY75&TZDMw;#~ckcr+70m zUw4PNOiqn8oLgx6u-i~f(sf?LBF^dWl`;v=4(1%!tZ$%T6a%qGlwXuElk~$xoRh}y zLwCtng#6N`bn^4@NhMOn>cclgC}dZq{=qHgQy0K2f1w~Wx9fDrW?qGF5nr&2(UTs_ zt{Wv|kzdg?*4Khq1CA8h1^RbP8II%_z7ZRy4(AnOPK%e&#;5ff)Zbd-lpZ8m(h1+*HTuDe&%F ze)%q!+7+mT_VDMQLaHsH`?csQ(A)#={;lC?y()XPuhnnj+jjMuqkY0r_Fe`)UqfnCxsDZw4tkj{1#5CH4UM7ky|N2B;2i&CA6_+@z8 zo~00@?ueWAfl1dbs@V}}4&sc<5a~xvzOGZZQIWEHQbtXGk<#54$~;sur;Q7A!xwa%HAaSf2 zngI2BrdbQ``WtHGlKMIoS?4R~en-1KNoY^aRcv3Sk6)xkcV!~V4h|F72@85QUDQ%8 zku}B6@JRtgW=}EyC2Fuu9_aR6zTRmg7x|O9&$Y~z8_!zsoqbs|M?1SxC$&W^LV~l+ zD74J)sTZL%k8nX5=C)#Z*#jVOVDm) zX#RXvOu`+bUMoM?o1#k=N!O(Pz}q>7>Jp|H=nwn2c(&PB*4;{a#NX|YUSBlCmHkY^ z(i0ZHDj63%U?Av3B%vs7Tf9c>Ln3EVz1K~BS1uPHS}L@@K#S(vh4rx?mZIKTm}rb+ z5^Rj{;o3Vkd^%=dWZTa#{X%i8M%l4O0PNF%ecZsv_ZnTPv8VKANN%w?NEY9~wQRhU z(N5RSee3zAd@gM)3x`?nklxk~EAyOI*}iHa-WWyR{9wOeN84J$Xj7Jqzml&G5Ma(W)xOVJLBRlN8- z$?qAyNzN6H&*RBkHkvk%_@zd#JZFc_g!4h zYijR|L!JykS6Pk%j5Y$FYyZ1YB|0BZP87*G)LygGq^-l)>A7pd+>v;4x6+eWA_ZOk zqEq4`WmWe#mCDbHC@h%`*SM~VSU>(E5J4|26q$;ow}g|vt6#&f8KxOshQ5ee5;E04 zTj~CdqXuu1G$h+UUOO0<@0yM%zR6de8S&^Z0iVM~4vqQX#o$T5x9-cmvM$TAaJ_-9 z#N#|-(>sraUA;sXE!)xcF2={`96DXFK|BrkW+|FfO6xK51>oalIg{)27w>$gjl5`D z2I4Jrk8Qf;Uuat=H}=AN^BU|&u?svCla)iPELT#0J9QC!oiU5_9;R$UV=IF!QD)I5 z$sxc^d)j*ubOW#;!@}hlhl&K65Q2>q%xEO=&}=I{HgtR8uj4iIC(r4bNV-j#8Ol;- z=iPHQl~87jUqk1jGz(Y!FZ!G-DZpJ%VlaWnA`@b!85bKgdiAicW7V^%P1OwWT0EiJ zB`Z^NKZA7D72n#jTv*;~SMlC4a3I*4^68y|xw$x3ztO^P5sNd0Lw>J`YJGCpGrZ zy=5{58(JA)*h_eLt&QX`wv1A@LyZ+mkEUUD>#aD?CNI9mXJ@`8#Lu$p})#zYLMFD!Y$-+Ak9P z9{1Y_&p1f#XTV|lT&XFNcuP`oQMSvj^D+!S1bfeA!6>-Z&`;?SrtyxE3d-qpVEUwG zb+^RvY~tbqGB&cl7uJ2bG$i9yRn|dc$~EJ!DFUSJr(RmWQaR#-SdvlWRZZ>9F=A@q z(~}5Bou}qg>JK49lRo_u)mAu<9vXXw_wWf{S11Q@w|T$UR#xp_L0O(E1Ut06uVpTbwNFhH zFatjFjpCs_NONO7lk`{l*VWD?I4L)z3rM(1%I2eyw$>9LAA5CBv^3f@ndW^O^=k1- zo6T64C5mv}eoR2<7cF(;Y=b^XhNlI1o8bUn;MiQbU0f+Sl4`Hy#vlQA>hyW0Y&0{* zpus38OOzkxE%egUG`VR;*(LwIL1$wYwFU z_I0*6kiYfNZ`POgQw~kj&(pYp{c33%2h=F47|zvd@0s{ADe4t`u)Ax;Y&?{=LCo2R z>p}IaAQR$K^^M@J;4F&$OFoGd@(&4x@8HcL&O@w+eOn@=MelvZ=5fQ-wIak?T~N5* z&Lj5#v-g!Uvg&6Pt&y?#T{fE~P+OOiU$!%w9+d8wK8f8_$1uk=XmnJ43_>T6oZ^xE zp5)6kb{6NCnWH>fj&{Y2%Bwv5RxZm$_G=R5=w}nigR$X)h?}DuUhunBDw!5=Ci-5m@KqC7TxH7YDT1r#k6UaB6ptMw9fEOjt$f@DM#__B{uS#JG?8197+sErX(=3$MSdzH{}Sm3AX5nkzm`}LO}HKV9OU%l)973Nn3WvB=shj_%?pgCe4*<` zz0LL&b?!jD%Ykc(pN(Tt_fBlbgI4@-q0gp2gu(kAnHUb(f7E$jt%c}LH@f*3<`-xN5tcxOFUDN4>kRbkEzmcF=Nh5b3>?`#-gWfj4MpyR} z!)SHYK%0av0GLdt5BvXLBA=Zf$wYKYc`H}g@2%@H1+>UqMfxknj-=d-3 zQKj!k#UrDFJcC^<{r8a_%c&}q(EP&Terb|Yvnbj5W!npx5P`K@glt`L*I7w)QReR{ zdFh_D1(loaWZnhreO=mC=;9L=XH-5g9 zm+XI!4Qt$X0$&`s6)s=q-+#0<%P$q~W_`3|l77;%8$xa-1UfCRD-D(n-KQ8 zUwKP+Ylbb{cd;Kr!QaiGHUJ%28?vJVnn*FT$o;c(wZVUyhk`17nj^bmZ)Xi+?gF0A z!T2q^Rh@IR90*XBtNZ0sq;d{#Odt9}#EIJ-M5ut{yRt=2ENtJ5+|(7-Kf9jyY4;@iDuK0lqcyC0*%Sg)V6 zd{snY^CNrd{lyKMlh-fPY6Th+f^FxU*T0Q0wd-Bc?GUOv>eQESAH3y*hj~_9a7lFO zyQF?cl2Hcg`Z;V)E^YRH%-{E*X7d2bMJ$%ImLecL)2_ z7N!gN_Sk6~RBAtYJ?eL}K^vn(Fn?7G{&>lqYu%BM)Sjd6#`scCUzu5xH>4`uzNv4# zuQ{?qIa^SFQo<;?ezZt=0U1+5DOB?JDL@5p%uR?Py1lPF@4s|zrbe?o@z3^ z8(@zw_aHYYjoGGJcxr&OM!YT*H){XpGkc6p+0Pi zVaw2Yp5%B(vrNeRNk`}eE;!pPx*8#NXi(T}wU{^omQ4jys`+;yLH(WA13SoBGJjoa zUo+?IuAhQ;xt#_I!Do7&0{07VhokVfM{F$|6QuJbzN&uBZE&^y?KfB7$(>p}(9@7K zlrsJ6n8N?7cK#6_!ltsl`2BumDXc#DOysY;FYW&pxZEs<=jO0a;~+Dz_YW28@FTs= z9UHB_p(pQ6cq5_|z)stbUwNeqHDZ97r4$OfUWTgRy%g0m%T;x7m8|&KbahbJD1PvI zW|%xa=Z9l9x=kZqp6GEA-)ZO{BR)m!N>yf%L`Zi;6%jK(ej zr-KRzP~><=A0MQ$aGM>F03V2?eq@&63!01XBiX;HWS;af3uvmeRHf09+&?;OqR-X` zbFq_W+}SE$4BRBFZe*xE0oC(Y{sItxv z#@BSuB(=c__kda#)q6k_3n8pl&ygkZ{&rE$)Q`+Uea?`0oY#G5*zr~lAuwcT|F!pS z>7AQ^5%@qe^<&gyI9&@ft)YY5(Db#)NL^%Cgj}B5aacxF;#x2+*wtAm%2X~wj4Vhu z_N^U;x`ACCgb)=p?u3L?NtyiFvH1jS`7?`UR^tnKmVb~W6#u4JyF;O8B^_W?sh{# zdHP5e$pNEoRg%H`g>o1}W{7YGk3+iSsF-TaXR)umGq?;2$I1|DhJ~)>vZnVQ;f0Jo z5KzV1Nsf$nTomF=aSfzVKbEUoH`_-VFZEN-PT;y%dh!(86H6Kzo*a9Jnl5Z4;rs~R z>>0fJ7%%d~7pSFUI3J<%(W4TLVo_gwb`?ZqmHp;?YD4E+HbyZYErW@LBRN5Pt9A`1 z;2iiWNmw$85g}vx;i6|I*ztYjiEp+E6=Ro;Tf0n`j$W#7CJ~9(MV7m9g6X3J`Z%8~ z#Kx#y>~Sc?p`~|+paJ&eG9Uk*n52!G zqaT?k*-ZsSnI|W0UgX&DlEer(7w73 zQx99V<0pJ9{_^y%^X9k*8QuW?oRY&W4-O^YtRYiQ-`Y-$1t;z5t*fH0w&<$v`Af)c zmCowUx4fko7yK*mc^QvIc}u;n)k2!%1gA!Ft%Y71g8&%cPrIF`8_qkvQyga|OqZos zqQGv6ITcqRRvt_5AlD!eu*j5Z^0Xg2BKNJTgd6Ra<3RbIO51@zMH|x8^&}Qq2dsn> zo?V2usA7)7?jbdPRGJkv$hJRk9EtuQM!fCJgcEMZanIO2(C8pHt25VZ*|#m*fY#jfTQnfhbF=nky$$iHyM42}Sj z=}v&W+4(@&;Enb(e@xWU#7~n!L{8krxJ)HUU1Mq;E#5JXdGekFd0pT2PCQgcZr*e? z#Kd6K2^jaw!Yq9fb^hxW%eyobF9lmTiRrBT3I0~#!&z?uczZkCI5waRhh4ft6z07} zQwVm%q`k~C&^luGU2Z(=Y;_t2Z&Hf=;kIKKBN$e!w@vA+6=x%HA9cNgaN9)AF{hA& zOBVz8Sfa7V`Bay8jS2dm0tm)s{whvSqK`-%5XW8|29Y#%UW4^Lsn*bEX~!8qf>;|2 zDz7S|IRoaMuR?PcoSRo78n^neF(ra^TxaYB;)^j{p^gAv4(PS9mmrb#9^`Zl!?N@P z3a)u}<=!wFr#!+=nre*b4VNxY6uA*FHmdEZ2#pT3Xhy~}jN+ZJ-;^mtlYN=8ad&W} z*;+YiG-oypV}#pKrQCCON?5F~T?sCmGC$x8iIyRPzf!~IF|xGM>@7R-!_DHWK3bVD zFfpEyRnvMtU|b(J)p_7oT-D)_C4H6dz)$Qv7WL0oU0v~$0R+~)I6KYdOEQ|N4}O|nQ^)QF25=sXX@^e?*<~;Y+f7BYkHH&!y@)%T zyu_#na7^nIv#M1tN$OW`B<6`DZ-Xl`<_$_M4DQe^q#nRw!ce)|oy448^}KtMw6NzbwzX zlB$)nbRG0)qn!KWoNB_n`otY2*noqJM+Wrz{d%9he~7hNh|F%3o#E~xFXcS(_7G}y za^FnS*|@eupo&yE=TtAz45?NPXo_8SEq;!6yoQCivnuu~k->(=1DZha3H`KM;#nD3 z3}uNold=wmBKI45RVC(tF>ZhIuTcN*O;F9tLQ0-K#o_} zFxIp-F6!DCbrgH`(Lr;{++G$r$Dw?dT^ns~4CX|9`Dzn+ayG%VR^tFhnDYhFjy|@T zcM+C?;SL1iPW@l-Hysau2@JjX<5s+J>>7?|Q$*vi+$q}A|aTVit7_z$=u5*ps zuh(e--(e4urrq2kyT)_;>Ig?<)dA`OXjlF!&qGzxm`5}CO2X=V*L~Y``?BJ%c5iX^ zCIjg>pTfTuYO@CXDmP`zTcSddVqrdRc)!|BFbv+|QW}nHt%U9qNlNAAZW&NX|E6a; zXC@Z*d#3t{2|3D)^+wJaM~)o>%_TuP^=w$CHB9_M(aOyPyLmL9CeH08GMksq#*!KZ zuSUjL%0%={=Z`>1GB`+omMD{cd%yCkM4UZU4vVQUhUs{xSAfcfTOnSfI~QCd;t=`yB#pG#x91qL}?Pf z{-wKeF8(eb9&E|Qv@;4&I{Y{;m2WBNuPNZq8(zedJuqgRYUqP~)43xBf-T-vs^0?R zwUTgAF@9k9>@~t)0C=+XY?Zkw>MA14+-Z}4Bf(5z?ke0vhtr5_k6HAwqXnn^m6}c) zr(q@ApwhHH#e!+EPj)6(liDq_GaLx$(wvkX0X=;}i(5ZB&cV9-%8)_f6a;(<5PA9- zzd&wkTg>b7!7lz_&p_i+r6njjW+vWn#lrC8Ey8J+Xm+)Em7D#}gcl{5Q6b$H&4y)~ zev_{ZA5ZNz^dINZvPXc_Npmo=o-e9MTK(+s>Bu3^n1PLJImR#3A*=Bi~aWTA@2t}q|npjuFMwMF%*BNM!8 zqA$CtnccZmaB3XIuP<&p{1eQLdHT+3NvMXrk%Y2qpYTK0Cf|)urFCseRL3ti)!xFS z0M}Bg`sYZC2@;x6)wacF#&Dc?p55^(DrS(9h)BhmnVbo3=nib&MMAd1U>H4%TH?EL z_p$*Y3gR&x&2um!iZWFdy}ogM*?gZyg=_!aeQ@ zHHy7KSM~04Zc41mDq@n>m8_#vV{#VI9;T_eD|yj{*I90{zg%cWn_KvJ?W&=35X}-c z*K}&;>a7<76A6{fXQ<6m8sSUoKbi{zhdsg0O%hIYaB>7L;`X3;z0)CwuCsY?we(f`;u{(iZEw`xx#xg?lb6C`R&e7S zZG=3yk8mD+e7vYFW9fai3mj@!O10N-7BaooMIQe=@397Ru5v%6m7PK@0Nag*v8a$P zC09B7^=yRqSk+qXBKKAOymVn2ADmsPk?gamN==Cj>HK9)RMmW0i6S(nz8e*@1(E%B zR;HcO$aeheE;d6ho?M@o+RSqDeqZ-~q51qH-8aQ zg=4X0iqI$3<9BxB9YkJi9%O6#!eXMK3E?qV6dw zdLAF{zXvuCrj^?(m7NsTeXn+17j0iKtZZDKGgYL4rVsnd;m0`DrB8n0Abe>!tX3l* z-FDT^RMU&OYkLhbSKsJ4^SQsoO*@4om?3c+{2j$oflRqSklg-VI6NE%;D^)ve=RuP zEBV~sF?4^Baq)8d9)y%-)qGf8AR=QbspD7U_pHyg8v0}5=E#VFcuUUgXdYB1MeZ%m z@7|0YfCc-xsn2yec)Z^f{A-rlA|8?H67jY@(NjF-@D?3-H7+~jQs_&(K0ieyIcuJ> zWt8boY#y5#8@OmsnA>;BXijbQg8^f;@{T4S>-CYJ+m%-gn{^ zX{BSUIX>L36oti^_4|QcWk$iKLK+)c(5_hTFk}>e)FE0ICL@$!5_CO=T!R5Gt*u}6 z`hs_jeGcjmQL$BeW1qL^KOn5t83@Bn|R=3KQdQ zMX7HpEh<6PqajJHfG+rBn+UD$Txr{ZpF{Hqp&R0#%&tU%ucCwG*v(AD90+2x9HU7F zIOW)CdZA*Y&H*<`$(h^GiGQ#jsoOkZ?I()R-ZeX(jVUr`I#v{I}OBw=*!f zU+-ff!G}uL!UQpvTaD~4VH`*4N5v}f}b2E_bs?;M$TN~fYg*3C963l ztd3%bymdw?;%dc)qArkFP-}=9uq-eq9A{d`h%2#a?@P)~~jt1z)Ef z1QSS4;4en4HDpx@&za(iv#OMfvQ18YGY?)U*s?)Bh6*&kx+8)e35clIB{Ue4CXJtb zhOQ%bO02&NhJHeIdNF5Np~mJJPTBFtiRJ$~XG-3@;Ab7a|RU5<22%m?CezVp|i5*wKg_iFdE zree|ZWrn@E&1H7G=%g58+G>k#xt~qdF?=NFnz~)g z`4}$0xt;X0RnTs>N{B1wf3!|L~5jj79c=?oC)7^p7ULti*tYe`zB9j&)$3X>{)Bg zteN$jVKRz5KJAp*Gq@?Httlu<&^V8SrQPg^V1@dCu6-ZCvTSu8@<{P`n>A_SbG6mX z1}gUZ*Z-wOV%I=_cR<>Uw2O=i%r{rQPG6jCd*zJ(QcF;pJ+G6b9M;ScR^^yiPBoGN zllsnr!UwDR*&oKk2zH)!)L;284bf<1Y|xznhaOyME3>=?&5HT*&{^JV87?$j&vBuQ zMUlrb=|z%@U=G^2ge76Sv0LB1+&V9H{#NR=B=f@~$%iF$2U3y>T8_c$PAqQ6KOvWWdW<3XD36)kHeH-S*Kc1r7`?KC1If^2W4Bo@Gd?Ey^y+ zz-C!0>te@T8}e$0F@bj>)obL5#_(|qJ1kv6$0;v3Lz<)P53Q|>{V{qLQoxyn0?y*J zgcxR)zM?C}=c+96wVtTdr0kL_sw)o#i=%uoURoBp5ap8drFRNlx0-E7vQV?l^YX~MyK`KY~?;!qr|nl-sj7_H44qd8y6Cd2t9+f=*mZb zIZC|!^E?7&h*BTQf+%Nnw`K!L$=sJv(b4A z)r25?eVy_=posp6syVHG#28X&{RTq-h7q&40Nl6ktdk7dVwvrZJICQ|XMv>q8w6b| zKARgl^*psdS?|`Sd8|IS`E>69^!G#o0W2o(5^fu=*7ZG;Ef6r0q;FzQ*TM|ClPMMh z{~CzR@SwNbr#ELkuq}%3)Cg4v1E-u@sQn;vN$pyg7oIW^!tcm8_eJv@bU0z{S41DV zRJo>dZ1}qJecrwNOrwF94Zs4gp)a$aH;YOmD4U`E8yi16cHsFIgTMUITJ$LSGj_Qj z1ff$vQGwFaE9#k^skcI2>-e^r-xh?TS>kD!oPo$|_tKrL6Y>t^lXqu)N0K%_QT%L@ zuWa6yM3j`C{iQizYSwCfZ)>pDqcJL1`q3y;9BPm>IJm1UlxEhgD`ejbO*IM4%RfJgo`e8m)+m&Ks%^5?2$}O4*+dOnhbzW zKhU9OePjL`o3$Y-Kjf$jKWE=b;N8CTG=+b9V9u#(kBZ#c7JbC{9iYpMFOUBoizNNP zILuG3p!$?xYXIL@&dLX3yl-M2%>0}k1P_Ux)nK0%g$q14S4ORVW5s}&Y)1pm8?*&b znjE4QH*((;fA`&g(6_gf{xk`KDp-r+_lP)wBd^gP6#dULCP#>eGIHE1@F)~r2fT)p z*V6v1%}bcjio0a_4dZ}b`6PMwMEyN_tcJngZGa&NIf~%zmMg1IGqirm?#Od`Fi*zw z*Bn@JaWYfhKaErUVBU=pP`>1k=8G!ooR2sQAA7xt5H# z@`|4QD1h=Q9W*?#vsH(V(v&b(Y~Kw)M^p^}BA2`@vnX}C&O&cF2J*)=6bo@Fi3fIH z|9i~nx=Hc*SKXD~*3(D<++Frer%@d<5U~zTr*n)H#WxYi{K5KAy&f%4KZya`$B1b8 zaPwH^3#^Ndvo6+_;}@0kJf#Lip~Kw)57Fkh@=`qHudg^8KCIMU?S@mSn+Pva7L0lF zdbN`Ab)kzfmc~p_yO8O>k4OCm&4P>MNml;B02{lV2l+D{9IQ|ezPh*lGi-E=(Y3_8 z<2llsQhv~n_Tcyi+q-J|<8lFwLoy zTejMxpn&<_VL1DdhA}IU$(tt)=N2<2bIWAMT7AQqLv#WSwX#EMDz0{89lL!A#sCBW z*L#iIqRw)$1KxtvMYH2PHSGV=aafO5%3(TO660!NBh(51v`B>z}9M^>vY?XoO63jk7bDl5xe$%foIv>z}n`q z*)!XR}$U|GkZk-MO}=0qny zN@8g_-~3VNE9c4I{rTGyzBoG++HggkZstOX2$#C+Es5AiI2YmqF9nBPBdALdA{)c(%sWell7o?7%?;*-+fPh!y40z5f{Bz!Lc4OlO5Udqez%YZdipKsKxvd`xj09Xgp%6%2- zg7?UxeKN8%4S-H`DpT`T{rp7Owd({fap}tPwO>i5t_}k6)g~>$ zyoZt?f5So`22UPAmRF;e)T6w{S!pmkxcns#PkOA3ObO^rX@DGnBe``JK<$p1Rn&>Y z$9~Q0Q~?s+BEJi?^Q|2;B5kN%gKj2`MoscOpwiSHLrb_xUXvQQ<4f29AeceDdvZ=s zyqQJn0vilKpjic~57YDW5a1@SGL-dSsGk4UuK$BX_&+!ZdpxL_f6?lL`y^mPj0Qev z)H=|t#dg6~p2LoN-(Gp1O3LMQ4D;hZjLEk<%uF?ceo)bCiN0RTD9Wzz$%!?&6CvT0U70VWt$E)>`cYJ?!YjuoCs3bE=e| z4&FRD`zylgVMg8u878G7K!GwIflPnG!fe zgh7D5CyHDaG7fsW(3vMNYG}xx9VYz`!_;jP1}MOh{U&%N0BoWG*#PkK9hyoU>v%i6 z*JxyXK=@dmPE(Hs#t<{x+^}1ow@~!=HyTl zuR77urhR*jc&{uNrL~{^VO0;_Vb8j652lZpjC%>d9Wo#^QQkUzok=TnE^EbwXb8P; zFaG0w!S2WHU}7%bHw%Bz9ZtUZz+2Y(p2C9}0_@%i>#pKCH9Z5SMcD007I`N6n}5%? z-j^NST|>Z{1~%hDpcawuCkFP{?^FaGc#bBNF^dXEl)0leuk}R|Cy7^X1vFdy2#Ejm zM7hQ5n8=3&R@luzp#ea`pLGCOOoiT<8myD zXN2DYY!nOjJ8CSD*Svm2&fML zw0hOpH~nDu(*hSH4)9Rs022e(FqD{&GBz`bdswT<=P zoOJ|G_8boV+e;=j*v@l{Skj@td~bkjNs1h>hC29`Z8DI!RPE}_S2z}haz|}t#xvLZ z*V8!)F(&u6X^_XFBCRprb?+MIs9Y^H_l`TqFrYzHZ&1K$FeK0iCWf2$8q|EHxZ~~U zoC}th+k7>C%c&ck%Ph=OE&6RMj*#)ZS&&s_SfB(W8x}7VTw^$USn?yxVI(z?ei-7OxX==rJ#8i)1^qN?5%vlsTv3;Au5jyzltp zgCxX%Qu|vAjkD2{2LlWEu6%V>65he;ms2LLPWVxj&3ep^WD@4U`tEUn?%pPH8{6Nl zNjGRSKXM@K8Te61SGPrW)QHugzrWFRnty@99V58bjIcvP3{O` zs$SqQLJ=F=m2x&D1q3BIdktza*-P*Zp{n;IC|S|X2BW))38Feq4O)5Bk)N3(gseGg z*yhYSjM~>RNZm7)7?m6UKv7!EEVfvLAB@AiAzI)Ko5-Y7fn{DABy0AX7NoiuowmBX zzd~(U+kI~K_Jp7Hv#cN?;F3zpqf!F<-kD#UfKeGzqQ;<#kW7zGVPr9X!GBIx;=>UILTS5pNbR4WnqQeVM0%U*h3 zz3~kL;)*(h9ypV_>hjCM>%;whRHRS_!CV+%khSk(M3+*CK*Xwm11CsQO^#~V0p z%UE`zzqC0@#Y|E+(@XRwH93bgw#II`%>3RTSqY@>fl&`6FPGKZ!pGSlCf^90E`FCX zT>vR-9Mh*`dmi3RuJ)OlhvZ^xi#{2pq?zA#l_XV-4kz3t)=SHGM;T!=D*Q6d;o`{} zb{DfoyUFWA>2cMdXLLW#F0Z&10G6W)osReGT;a~3qCS#>3(mSVti!^6so$mNKj)kX z?#GW?d`y3e2xHR=8Sf!&;Q$8L3N_{>c6OyagfT|WRz2(>A&nr`hily1Nkg#1JAg2; zFn4gOiidW+o$qU%{D(0i(_@^QuYgl=@?n8|-m;M0jBo1u72kt7^oqvQ$hl0nq$l58 z$aP91w^m2;15x%Wb{a8WLcWq0Ms`i;R+ zV+mY94I*BvD0*qPaag#q_AJ;=T=FjCf2v+PYWF#=Cn_WQd9pdVk3 zzGfzF@oLUhYOQ0>mG58dV->XhSG}+m%O_q7JD){02$o_Fy6sLXK>Qb$PcNUD?8jaH_77pR|av9xVjw4y! z?a1(G+q;S&-;4f;zyF$9m2wp0^tvU|BEJlD)U7$SzftiM`~Q9o>J5Xa##Oot!mr`A zoSgZ_=g%9cCKv1fRc!t=mSxVZJ5qL&gEhj_|H!p0?j`rL9e!%s#YBTHmu&m@Ig0rf z^ACm#fSJD;Bx+H7%KXje6Gfa^QRqsC1;0J~$}nz=aIQIg$A#?1K0S7uu=84I^@mnUWF&EW4AS=BfBx%FCF$G7cl8CS!(2P+PG?vnQvUqD>&B7c4i==pA+CRdT&~Pd1@QFM7NI6|805pGlIbo>CYs z)$cIqjT9^neZ@QCT9FM54}ZAk_o`hbH1&gC!c?YPE^i}|AL8@<^#XIZR@!ZK;P-0f z#`H81v+d0O;C}QtvMPsQISUHGBtySIXip)gE$PI#)`k)Mypyt<3sYIcZe-p|m4p({ z!JjIWE~L#N!v*r$91|?0O-j7al~jqTO6nv6BjcqN&~_D1c6TVhRd$y9%7P^=fkuXX}vnOwX6w$jcyZoc+fdIgDfKH{KdlggGT-_FKCYz0LM*iCk;=dlCI_6< z=lyaCNM{z($qb$5wTU{i;lW96<#uK zkke|<6+3u`ZsrT99P;wZ8=W4#wpmIB2hC9~uaY_8sp9abnu;g7jeCt#KDY?djm`Jr zUlwoFnrLnRSsE=NH0Llu9;q?+eYAx7<85n8O%%-@^Uf~5)|c&fZ)ni%ZHWERFoPdQ zMha|x?RrfIA)7Vm#>AwEnxxD*^O0Gnj&B7f zkFM9YQu_1SVw;fk-fwbtwtuir#}!x;OE&r7fNshlcqQ=B-y3tq&Y!>fIyBa(BC39< z>R}?;!t7R$K~k=6HvadxosZgtX_P?siu%L*U$HO*lM4nK4%jA?Hwz{v6W8=j z5%#KPoeSeGpFNH^$}%R)YsbUt_gpffCmn}H#~anf_W$Zrqa~b|G$FY1TZ?2hyc|!7 z^X=;HE|-Dp2d@-Ggub+^^DN9fO!IqE$F`ZPsj;VNeC$)T!KJ!7EhX1Dw|E3;sD-`d z*9q-!P`!6pT4I+kYG<~Q9_yAHCJr`Taps<>cWQmp(Gq$a%Jf(03Xww$UfD;uCHT_e z(B}pVhLCpoUiCeRv(r6A-3Im`L{$S!mI4wiLRc8}X)dI^ECgF!EfWE|dP>Uef@?CF ze7sI8z4{Dk7ab`&CC7a)ZO*m_d0*gbO~coHif;`;m?LX4(|d47IsQoIMJZ+;modzc zOdH0(Q4I>DxeT=XoI_Qlx7|k`zIoeKO#0mQgVA7lT0SKQ4&tWQqaZRJw9$Ez+BRwk&PH0oow zTW)r;JckuJ1A=8CsC#)K){T4Xl6$hsbK$&a+dcUml8O>LBSDOXPn^48mVqX_;j>QN z7>|If&^a0m5?*WV%Het@mDmv!ADvQsPC$B=^h2^;QV$!sAO9l>X{ecg3+BUnrRVB} zBaZGgHxRhOZCm*wT$muBIFHH8<JuF6{%Xd&a~hG`%M>DGzOVk?+{1u5i8;^A z$*DTI@qM*wA-W8}^cv{RyhPx^ zTb+H0`JHr}SP3tW-A~oT3CkrmF;aMZF(l7G2Uwi96WQ=z2(oo-{K7j1C(onDe58Eu zre1^$56PX{Hb;~2QA*Hz1Nxg*dTN*bEFPhOs|)VRrTvZXO4M!y9VA_;mwl4D*Jt%? z(Ei!>z9j^`I=r+?yiSVRwr2GVkG?NJ2g%oJ(1@PUZ7$kV!7u;+{=FpfLA?I&>_Djv zEkgxhGT%-)r=P?KwU{6Lez84nsq||T+|>w-=?oWInS@^ftF8gTxi?1QwseBZ91=s{ zb4yK^Uye`s&HJ!C_709TG7G)mQK@BZ{VnS-v9HzCmT;U2O7R@q*~n7?cu_|T>;=yC z0W)v`{&zY4cX|H5|DAb7gO(8+4ki$U;8k_#6I|dC&*#G@=|m&ufj_Leeftj$f$rU?8|qDZl@)NR^4=JSHVf3 z`l=~^o*WOhLj$(+!^yoj;L=YC98A?GwR=93jjKQ;hJQ1m=LTc)@hAmw-PD5>c2_ZY z&d+@^9=#ZjdalC6=@=Q-w1_R*_#*{7}N@-IND|31jCxs`j5 zN8F~O<^cfWhIg(0q{L=2i%3=Y;!$>kC3G`NAYjxS=5zjX;rL0~=JHAj#q>T`jY z{U-e9ZjHm`V)}Jb_J|B~zl872)PbnB`Gy!qrC_-x7LnSZJl8c5VNSqR>iru2@(~434~s@>{-l zWO#p$4_>QNEzDoJ-!RyGeLHl<8B~Vnc{eRSj5#lzbKI_l4)A<7(zW^udyGTiHEAhhhL00-qtzBjQsdu(&-}) zj_pn9km(56sR5FjofnPVE5?&N{qw*&*$@^*F(Qy5Nzywo6re?|q7nH7TkR_tOipaSWdoN{Ipw)Z3RW~v26@CRz6>Gx3k>_TxzeHvTmC} z_&0TJzs_72Orn5brCCx)xCc3;-S5(8J4?#UKXCv%>N`kWK~tl_K$EEppL7h_%*J26 zpn#b8qcGcq5iqXwUw1GJxV>rU29%f;r>Q2-+ETrv?0sJCVt7sp+Zpomcq5l?Fj@`A zf^Cv%!#3e)!eVRlAf`q|GPIjC@k=&`cd!qqKURkp)IDgYS^}PVJ- zkr>&II~s7QM>GrhY^=@~Z*nHncY-eK{FCpt@+lvjEd9u=LT0&boh++0tA&>TqBTzX zH3Lb|Zd0$~T`SG2y_%6xXvV88B%>L6F{2R~+TDq{b4i569x3a6p$!{(u3VQhBVAoT zHO7goa3Rph^UkJfw`dqisCG-4rL`z(M;9pC|Hd4)JT-1Cc)bC+R=-7xgQXVaLR8 z>G)w9v-hcn@|F1==bo4^>|jO`)bV$_b3)`-0TPGj6~>gO{63Q7P58IK#tq{!?+DRR|&=+N9l@~F~@RRtpNV*<}%K^-HbF105{Uml4Ek_@cANMrHw;D(Zrc^kUn zdZ1npZzI4%i9@*KDzw0yDL6i|OS_JpSx>0qrrhrnb`{u5;Y(~u>cWQ^LPq(55)$u(+&*#$EoXcD*vaXm@)E~K zSM4;5WI15|fYj^DzG4yX!?Iw%G&e_yj97q9JUHh6sM5`V#NRu*!0>ExF@GuGkQU~oylS8Dojk;PZh9;y!;eU z^7ZSP!(3Oe?U&4EW6WwCC0N~QDULE<9)4@TG+yA{D$DaL=kQ$}Q{QE(L>igh(@v|c ztCQmf+qluPBD1`jbKQlFxanqMrlv}JY3db%NE_cc z$Yj9&9)<^K(z3xV9;2{(ve%Bd(lXXQbd%bOp`}A<7A2}eMClK?)-H6;ipLL|WE$`t zAuNA}+ZBI0()c^fvGA}pSluWrW)n8cr*mG|h|l{^Ni%bbJeHm63~VYIy?uSr=C%Td zg?c|PdwO!Z^0Ur6x=;CFt{#l7B1=C%o;ND*w5c$B$Fz6tOCBv1>QyhD3jL>SRiWRK z&bo^Cu-)<%aT8Z7xs{7c>8pN+(OWZH+q|Id$_W{ruWoP(Ns^_MbSDmva=3I*Zf_ASH*|Xb-==REr8-EF+jL3S&rg(=2oLM3UF3TA%ExDJ zAa>mN>fzpD-4^i()9&i^N~J*->|Mh1xrbT*5&59|R@)h;@6uV1OiXB=AR1s94!?wX z>0t?3t9l_tX0ZJd&=sdktsaA{zhcKrN%ZRX1$Yjd^=DmIZ-nfXNF&zcB4%s4V;7Q~ z+7oCN14w6kX7`9pe0SWw+~mGJ*)xo*D=#YqM=PE*nfS66f;>5>zd2` zI>VgCR^PMJOZIjFXK_rlyT}{4K1$3W51F2m5@`&&UuBx$aVV94(_{uEUi_*2+4YWJ zSi8W)aEsKwmPoq-s{Kb$v-Y1!zTwz$DF^J7s(oar=*a#O?Nmil=k-g8IpiVSU32YV zGj`f@Te3PW7m?vjCk>tF>9KcDUK5g$?TH^glr&_m<(DUzQY&4bY`B0bK(=<2bNHSxo0pUm7m!ekl>M zsT8#Yls9HzPC!mk>)CDHFH#`&p`{}Du3+O%<}R@A0D>N+rFzr|Ip;HAeXgGgYx=4x zNV8esi+h%++RG-fmI$b)4Tu9=oX(7^pI?(|u4EFeI}A4Pk{U(I$&zj?x+Co%YspwF zn=s-=gvpF6?WFc*?bOFVezPTl6zfr_%Erxd#`Q~yC<2VJK--hh``%S~LuTOPF%ilI z`VO|Ur0s`(0*Bi943eAJvi8u=_Vkryak#(-Gr`QW;L?_GH&~Ewo v12K@T<)5*?0!95n`>urDcY3{6iBzVWvNx6oB&w>wS?JbvlWS$Vj!?YHS literal 0 HcmV?d00001 diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/Contents.json b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/Contents.json new file mode 100644 index 00000000..c540110a --- /dev/null +++ b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "guideArrow1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/guideArrow1.png b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/guideArrow1.png new file mode 100644 index 0000000000000000000000000000000000000000..885fb7a127f358435eba97c8c5c70f4d5f503596 GIT binary patch literal 1394 zcmV-&1&#WNP)00009a7bBm001mY z001mY0i`{bsQ>@~0drDELIAGL9O(c600d`2O+f$vv5yPn5MKrIRp7Y_=v0t&0C(VY@CO)BiJg(e1^5sa z`o9^@Booha>%y_%Xaxf*=3+BhuE`zKt2bS z?edL*9+1!Zs}m|)$VRYi*Uty3N1Uh5b?2`ep>l!k^K}>l?U2ubWxIS}`&>UCpc};Y zIag@cxhz>auzkJ`m_O|~fMvUUnAbkPFvtdAUfCWRp&}tpXzcv;kH0#}^PK@M$U;GV zg8{y-Iv+SRt?j;+ET+BR@-f|5Gb8&%%KXCHNi_J0}s#^pty{ zd=<@^dw}#W*l~^B|VTJ?2wo>ye-D6yJL%lm@?sJ4*yMY{yBSjzsKeP2sR8 z6_y#t4w#Wy&<~3`Asx)$Dsu>ICo@6UEO1z)f%*3kEf&~U??y96M}^F;D&ur z#jU*H{QC3t4U7DL`UBEBQ$>~J-yu$+p3=!RL3(G(p|FyC4B`mD{r0{u+PmLeMsNoh zTMS3+SG00%kiH+e7b+j(Fldi7e1eGuV{lVef*cIAw_FP|V=rU}gIh7gB|iqYMu>~`3~r5(w#|m; zJz0V{k)aLlqmV7o?|uvu9-Mx(1o|Gc0~Zc#f$j)C2G=5_b*i{nZ%smQxy{BJ=-b_1 z$QI~sW)80VmPm`BZ+F9MS|MAYAMdt8wm^4)SI`UDGL~k|O<_Fa$s(}btye;6?u2Yf zD9yc)J<#olg}9k+N-ty+bTe12 zkWD!MsI`EdE31^{c_(Dfp%<-roZ6F+IfnrMJ@LAyrfHsq%sB+On|K|^zE(kZ%x^1X zQpn(L;x*q`p6v<&o@#%;2$>ZEJhh+sNyw}wG_Vt|`O}x^??NVp41PZI@6QabaXx`R zarUZmK9KiEg3E_vRG>NhVI1oorYytI=t;=DA14jvs>-*T;s5{u07*qoM6N<$g60I7 A$^ZZW literal 0 HcmV?d00001 diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow2.imageset/Contents.json b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow2.imageset/Contents.json new file mode 100644 index 00000000..4e3272ad --- /dev/null +++ b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "guideArrow2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow2.imageset/guideArrow2.png b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow2.imageset/guideArrow2.png new file mode 100644 index 0000000000000000000000000000000000000000..2640e6f21dec60b9fedd43d17136c1350cfa1f77 GIT binary patch literal 415 zcmeAS@N?(olHy`uVBq!ia0vp@K&-*R1|(Ix3Vs49&H|6fVg?3oVGw3ym^DWND9BhG zXv9=eZtpQev}lRjW5J`>eK`(1 zKNdc2UK{cB_piAgtNydP=55U5?#a(*Xi&CO2%NB>t5nE3@6e|b^ZX>&B@wo&QKto( z{w$VWa-jI(pFc@%-_P%9YVk9>o+KVMXS4ea=Y&AzTZa0M+*S3(@vCP9%v*isma@#u z$u72=&kEKq?P#0h*IfVkVtUgYzfVE!{oV5xh@Q-P7wj_a=Pad3^N&hy`5@MFmFo%r zqRDfP%}cgx5WBbR>e`KKMBCb>#Y{C9O;b+~b~$>5Z5Oj_ZN*0}6ZKWV@MQ3G^>bP0 Hl+XkKpY*44 literal 0 HcmV?d00001 diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideIcon.imageset/Contents.json b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideIcon.imageset/Contents.json new file mode 100644 index 00000000..6d1fa50c --- /dev/null +++ b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "guideIcon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideIcon.imageset/guideIcon.png b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideIcon.imageset/guideIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..d92f19e28d7285f9514c3106e323ca346853ff72 GIT binary patch literal 2850 zcmV+-3*GdIP)@~0drDELIAGL9O(c600d`2O+f$vv5yP)S}Gg^OKb{}a@Z*2P=FH-B`9!#GUNcI zEQN9l{R47;t7&Pp+MUt6;6~O)IB!k-k?GnNI*tB5Y8=9;lxVYl$H_EWAemHKuKc! z7i}cRO8m4c?1e?=#61kog5SNyAp1 znMm^9zVr=YNo{L}6oqKogtMMn=qaG2odD}+gvE^K%Yp%m*U2%aiH(;qT@ix80ID^iW?(|W;R3EMN*V}V8X;k z2-EPTc+fUsY_yTozCMwn>$Bp^ z2x?*Lq$r-`o>-_TX}%?e*4EZ&Wo3nymzU}5*R{^y0sal_-MhDQ+`W4@?c2AvbKJjw zf9F_tYAGNH<^B6bT3lT0y#DfKg?xap4jkA|M~)n!qeqW)L{&FxlLJ94Z{Ez)^XGG$ z4l`^ZC0U=zqTDK`L5X_Y; zmph^fQMI;B1`tzTVhw502?+TPN**8_+?=8s_YmT6jSy5GViZwS0?I3%qJ`59gmmG; zueoZ5v56=e0p$@7!V_)+!j%N_3jgNK8)B}VBMJeb4-2&|7myG?cyPbd$tj7vZuIo& zla7N*NBnpoeDSw$yCux{!e76BDNZ~9!hZGYrE_fy4~!j7A{p@la(!xH`FLt2cUrK zNI*mNVE0ShA*@g?fDpwc)pnGnG{iX&m-|3CcoIW#@Zb-9ognnNxqL{X3yE?d7x%$v zzK~>vkp=cxQ^~);rja|B+{h|hLksrQwooHnsR6t#1g$-R7dM7HNF0yJqb?T3E zMZAEDZY;xMPrYVI*hMmnB!&t`=>p=Wv=m*_fKV`z4?`+&Z76sxaVa_p*Hijd2fKD< z=M`l%*W<>(iOPm+rRN^LA}HN~xYkxFz0PG2Lo0ph0^+`eIyW?OtItYkr57QfEGx!S zSVtGaXU=jUDV@wBYGK?OhLUC>6b1L0r6lws1jJdfw7F14zP+RrSP?Fi)p!?r-&MQoPZpOi%UWPUlz?1jpki0TkYtSl4Bd6W?LcJrUzLO?0pSL5 zOSssba6*ygf+3gMby*Wz;wCKOmbgRmidqlX?h|V5WSsZRnV$+>WYc9$?9j{W`66VywBE`Qh>|4)a>B2QV}a4m$j3DOSqE=uGyt~Cghsf!$ACob1tC_AU}{W z43z`<64MZPFVV+^u;n^L z7rXPsu(VAS6AST0x`rn_?f8vNJRmv&0pF^-btDLC*vY9KHR;l$-V3QM%^Xn*2q3YZoSgKY zHwF=5U^;RQUuVL=FNnogG(c+7?@F`+0(q*%9JqnFnRazl6g#v7OUc1uP;gF{mGxbK zh~YdjFQOH-fGj?FQQ5?p@ARS)0s?s&9Lof*gMPlMEMHt10a=j9V(hXsA(h>v5Q@qP z$Rb5Zc9dnP@kC`)SpiufBn%8IdVW1Z6^6%F&2hS^ynrlr+io=Sg_>P~*|iW2CI{4I zf%Mu4C=J8}N3xUnd9gO^T9_1^i(J^nw&6AG7$QVgF`wq5sTbgcOAb;cFBx(p?$%NKxEq z*%xUCg8QI0rY@vpfu4)QN#$$Hfy_VP2kJr!$E+0(XrzClE~GHDO*xqs(6-?mi3_zz zQF_x#iw87%y~{Y!TXPsEMcHDr=9Q-3fU--Z!#tENpoNpYe$Z1uiA`xJQ!G0|m!0VARh<}gM|omevBIn2GuX;wz^ zi)Klw3$rfb0nXEU5-Zchy87JX!d9AG^TNz8zGV*6q*eSXf_pk`?s2YFubgtAZjxcJ zXl!hfQUw-Fxzw+!o`u)URGcVjl|+MMo0M3wu5;2yJiyC=(j;yK+e1}fJ7MF%j=Y#j z`aiuHSU{FUNaLp5Bn@p+8Jch`LlaOMBc^e4m@wr>QZbcHn>Hp*yfC6!pdk}PKxqHK?) { + public init(type: DictionaryItemType, bookmarkModalFactory: BookmarkModalFactory, loginFactory: LoginFactory, dictionaryDetailFactory: DictionaryDetailFactory, detailOnBoardingFactory: DetailOnBoardingFactory, appCoordinator: AppCoordinatorProtocol, bookmarkRelay: PublishRelay<(Int, Bool)>?) { self.type = type self.bookmarkModalFactory = bookmarkModalFactory self.loginFactory = loginFactory self.appCoordinator = appCoordinator self.dictionaryDetailFactory = dictionaryDetailFactory + self.detailOnBoardingFactory = detailOnBoardingFactory mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) self.bookmarkRelay = bookmarkRelay super.init() @@ -65,6 +67,10 @@ class DictionaryDetailBaseViewController: BaseViewController { configureUI() bind() // 액션 바인딩 setupMenu(type.detailTypes) + let vc = detailOnBoardingFactory.make() + vc.modalPresentationStyle = .overFullScreen + vc.modalTransitionStyle = .crossDissolve + present(vc, animated: true) } override func viewDidLayoutSubviews() { diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift index ebab42d5..a21540e0 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift @@ -10,6 +10,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { private let loginFactory: () -> LoginFactory private let bookmarkModalFactory: BookmarkModalFactory private let dictionaryDetailFactory: () -> DictionaryDetailFactory + private let detailOnBoardingFactory: DetailOnBoardingFactory private let appCoordinator: () -> AppCoordinatorProtocol private let dictionaryDetailMapUseCase: FetchDictionaryDetailMapUseCase @@ -33,6 +34,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: @escaping () -> LoginFactory, bookmarkModalFactory: BookmarkModalFactory, dictionaryDetailFactory: @escaping () -> DictionaryDetailFactory, + detailOnBoardingFactory: DetailOnBoardingFactory, appCoordinator: @escaping () -> AppCoordinatorProtocol, dictionaryDetailMapUseCase: FetchDictionaryDetailMapUseCase, dictionaryDetailMapSpawnMonsterUseCase: FetchDictionaryDetailMapSpawnMonsterUseCase, @@ -52,6 +54,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { ) { self.loginFactory = loginFactory self.bookmarkModalFactory = bookmarkModalFactory + self.detailOnBoardingFactory = detailOnBoardingFactory self.dictionaryDetailMapUseCase = dictionaryDetailMapUseCase self.dictionaryDetailMapSpawnMonsterUseCase = dictionaryDetailMapSpawnMonsterUseCase self.dictionaryDetailMapNpcUseCase = dictionaryDetailMapNpcUseCase @@ -79,7 +82,15 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { case .collection: break case .item: - viewController = ItemDictionaryDetailViewController(type: .item, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) + viewController = ItemDictionaryDetailViewController( + type: .item, + bookmarkModalFactory: bookmarkModalFactory, + loginFactory: loginFactory(), + dictionaryDetailFactory: dictionaryDetailFactory(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), + bookmarkRelay: bookmarkRelay + ) let reactor = ItemDictionaryDetailReactor( dictionaryDetailItemUseCase: dictionaryDetailItemUseCase, dictionaryDetailItemDropMonsterUseCase: dictionaryDetailItemDropMonsterUseCase, @@ -91,7 +102,15 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { viewController.reactor = reactor } case .monster: - viewController = MonsterDictionaryDetailViewController(type: .monster, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) + viewController = MonsterDictionaryDetailViewController( + type: .monster, + bookmarkModalFactory: bookmarkModalFactory, + loginFactory: loginFactory(), + dictionaryDetailFactory: dictionaryDetailFactory(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), + bookmarkRelay: bookmarkRelay + ) let reactor = MonsterDictionaryDetailReactor( dictionaryDetailMonsterUseCase: dictionaryDetailMonsterUseCase, dictionaryDetailMonsterDropItemUseCase: dictionaryDetailMonsterDropItemUseCase, @@ -104,7 +123,15 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { viewController.reactor = reactor } case .map: - viewController = MapDictionaryDetailViewController(type: .map, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) + viewController = MapDictionaryDetailViewController( + type: .map, + bookmarkModalFactory: bookmarkModalFactory, + loginFactory: loginFactory(), + dictionaryDetailFactory: dictionaryDetailFactory(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), + bookmarkRelay: bookmarkRelay + ) let reactor = MapDictionaryDetailReactor( dictionaryDetailMapUseCase: dictionaryDetailMapUseCase, dictionaryDetailMapSpawnMonsterUseCase: dictionaryDetailMapSpawnMonsterUseCase, @@ -117,7 +144,15 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { viewController.reactor = reactor } case .npc: - viewController = NpcDictionaryDetailViewController(type: .npc, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) + viewController = NpcDictionaryDetailViewController( + type: .npc, + bookmarkModalFactory: bookmarkModalFactory, + loginFactory: loginFactory(), + dictionaryDetailFactory: dictionaryDetailFactory(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), + bookmarkRelay: bookmarkRelay + ) let reactor = NpcDictionaryDetailReactor( dictionaryDetailNpcUseCase: dictionaryDetailNpcUseCase, dictionaryDetailNpcQuestUseCase: dictionaryDetailNpcQuestUseCase, @@ -130,7 +165,15 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { viewController.reactor = reactor } case .quest: - viewController = QuestDictionaryDetailViewController(type: .quest, bookmarkModalFactory: bookmarkModalFactory, loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), appCoordinator: appCoordinator(), bookmarkRelay: bookmarkRelay) + viewController = QuestDictionaryDetailViewController( + type: .quest, + bookmarkModalFactory: bookmarkModalFactory, + loginFactory: loginFactory(), + dictionaryDetailFactory: dictionaryDetailFactory(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), + bookmarkRelay: bookmarkRelay + ) let reactor = QuestDictionaryDetailReactor( dictionaryDetailQuestUseCase: dictionaryDetailQuestUseCase, dictionaryDetailQuestLinkedQuestUseCase: dictionaryDetailQuestLinkedQuestsUseCase, diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingFactoryImpl.swift new file mode 100644 index 00000000..6c0482f5 --- /dev/null +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingFactoryImpl.swift @@ -0,0 +1,14 @@ +import BaseFeature +import DictionaryFeatureInterface + +public final class DetailOnBoardingFactoryImpl: DetailOnBoardingFactory { + public init() {} + + public func make() -> BaseViewController { + let reactor = DetailOnBoardingReactor() + let viewController = DetailOnBoardingViewController() + viewController.reactor = reactor + viewController.isBottomTabbarHidden = true + return viewController + } +} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingReactor.swift new file mode 100644 index 00000000..c473e635 --- /dev/null +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingReactor.swift @@ -0,0 +1,52 @@ +import DomainInterface + +import ReactorKit + +public final class DetailOnBoardingReactor: Reactor { + // MARK: - Route + public enum Route { + case none + case dismiss + } + + // MARK: - Action + public enum Action { + case closeButtonTapped + } + + // MARK: - Mutation + public enum Mutation { + case toNavigate(Route) + } + + // MARK: - State + public struct State { + @Pulse var route: Route = .none + } + + public var initialState: State + private let disposeBag = DisposeBag() + + // MARK: - Init + public init() { + self.initialState = State(route: .none) + } + + // MARK: - Mutate + public func mutate(action: Action) -> Observable { + switch action { + case .closeButtonTapped: + .just(.toNavigate(.dismiss)) + } + } + + // MARK: - Reduce + public func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case let .toNavigate(route): + newState.route = route + } + return newState + } +} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingView.swift new file mode 100644 index 00000000..cbc78122 --- /dev/null +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingView.swift @@ -0,0 +1,193 @@ +import UIKit + +import DesignSystem +import DomainInterface + +import SnapKit + +class DetailOnBoardingView: UIView { + // MARK: - Type + public enum Constant { + static let margin: CGFloat = 48 + static let trailingMargin: CGFloat = 8 + static let iconSize: CGFloat = 36 + static let contentViewSize: CGFloat = 44 + static let arrowSize: CGFloat = 28 + static let arrowTrailing: CGFloat = 28 + static let firstArrowMargin: CGFloat = 10 + static let secondArrowMargin: CGFloat = 15 + static let alertHeight: CGFloat = 220 + static let alertWidth: CGFloat = 328 + static let buttonWitdh: CGFloat = 96 + static let radius: CGFloat = 8 + } + + // MARK: - Components + private lazy var iconContentView: UIView = { + let view = UIView() + view.backgroundColor = .whiteMLS + view.layer.cornerRadius = Constant.radius + + view.addSubview(iconView) + + iconView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.size.equalTo(Constant.iconSize) + } + + return view + }() + + private let iconView: UIImageView = { + let view = UIImageView(image: DesignSystemAsset.image(named: "guideIcon")) + return view + }() + + private let firstArrow: UIImageView = { + let view = UIImageView(image: DesignSystemAsset.image(named: "guideArrow1")) + return view + }() + + private let secondArrow: UIImageView = { + let view = UIImageView(image: DesignSystemAsset.image(named: "guideArrow2")) + return view + }() + + private let firstLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + + let text = "해당 내용에 잘못된 정보가 있다면\n아이콘을 눌러 제보할 수 있어요." + guard let font = UIFont.korFont(style: .bold, size: 16) else { return UILabel() } + let attributedString = NSMutableAttributedString( + string: text, + attributes: [ + .foregroundColor: UIColor.whiteMLS, + .font: font + ] + ) + + let highlights = ["잘못된 정보", "아이콘을 눌러 제보"] + + highlights.forEach { keyword in + if let range = text.range(of: keyword) { + attributedString.addAttribute( + .foregroundColor, + value: UIColor.secondary, + range: NSRange(range, in: text) + ) + } + } + + label.attributedText = attributedString + return label + }() + + private let secondLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + + let text = "제보해주시면 빠르게 반영 할게요!" + guard let font = UIFont.korFont(style: .bold, size: 16) else { return UILabel() } + let attributedString = NSMutableAttributedString( + string: text, + attributes: [ + .foregroundColor: UIColor.whiteMLS, + .font: font + ] + ) + + if let range = text.range(of: "빠르게 반영") { + let nsRange = NSRange(range, in: text) + attributedString.addAttribute(.foregroundColor, value: UIColor.secondary, range: nsRange) + } + + label.attributedText = attributedString + return label + }() + + private let alertView: UIImageView = { + let view = UIImageView(image: DesignSystemAsset.image(named: "guideAlert")) + return view + }() + + public let closeButton: CommonButton = { + let button = CommonButton(style: .border, title: "닫기", disabledTitle: nil) + button.updateTitleColor(color: .whiteMLS) + return button + }() + + // MARK: - Init + init() { + super.init(frame: .zero) + addViews() + setupConstraints() + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - SetUp +private extension DetailOnBoardingView { + func addViews() { + addSubview(iconContentView) + addSubview(firstArrow) + addSubview(secondArrow) + addSubview(firstLabel) + addSubview(secondLabel) + addSubview(alertView) + addSubview(closeButton) + } + + func setupConstraints() { + iconContentView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.leading.equalTo(firstArrow.snp.trailing).offset(Constant.firstArrowMargin) + make.trailing.equalToSuperview().inset(Constant.trailingMargin) + make.size.equalTo(Constant.contentViewSize) + } + + firstArrow.snp.makeConstraints { make in + make.top.equalTo(iconContentView.snp.centerY) + make.size.equalTo(Constant.arrowSize) + } + + firstLabel.snp.makeConstraints { make in + make.top.equalTo(firstArrow.snp.bottom).offset(Constant.firstArrowMargin) + make.trailing.equalToSuperview().inset(Constant.trailingMargin) + } + + alertView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.width.equalTo(Constant.alertWidth) + make.height.equalTo(Constant.alertHeight) + } + + secondArrow.snp.makeConstraints { make in + make.top.equalTo(alertView.snp.bottom).offset(Constant.secondArrowMargin) + make.centerX.equalTo(firstLabel) + make.size.equalTo(Constant.arrowSize) + } + + secondLabel.snp.makeConstraints { make in + make.top.equalTo(secondArrow.snp.bottom).offset(Constant.secondArrowMargin) + make.trailing.equalTo(secondArrow.snp.trailing).offset(Constant.arrowTrailing) + } + + closeButton.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().inset(Constant.margin) + make.width.equalTo(Constant.buttonWitdh) + } + } + + func configureUI() { + backgroundColor = .clearMLS + } +} + +extension DetailOnBoardingView {} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingViewController.swift new file mode 100644 index 00000000..1f1f1552 --- /dev/null +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingViewController.swift @@ -0,0 +1,83 @@ +import UIKit + +import BaseFeature +import DictionaryFeatureInterface + +import ReactorKit +import RxCocoa +import RxSwift + +class DetailOnBoardingViewController: BaseViewController, View { + public typealias Reactor = DetailOnBoardingReactor + + // MARK: - Properties + public var disposeBag = DisposeBag() + + // MARK: - Components + public var mainView = DetailOnBoardingView() + + public override init() { + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + addViews() + setupConstraints() + configureUI() + } +} + +// MARK: - SetUp +private extension DetailOnBoardingViewController { + func addViews() { + view.addSubview(mainView) + } + + func setupConstraints() { + mainView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.horizontalEdges.bottom.equalToSuperview() + } + } + + func configureUI() { + view.backgroundColor = .textColor.withAlphaComponent(0.9) + } +} + +extension DetailOnBoardingViewController { + public func bind(reactor: Reactor) { + bindUserActions(reactor: reactor) + bindViewState(reactor: reactor) + } + + func bindUserActions(reactor: Reactor) { + mainView.closeButton.rx.tap + .map { Reactor.Action.closeButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + func bindViewState(reactor: Reactor) { + rx.viewDidLoad + .take(1) + .flatMapLatest { _ in reactor.pulse(\.$route) } + .withUnretained(self) + .subscribe { owner, route in + switch route { + case .dismiss: + owner.dismiss(animated: true) + default: + break + } + } + .disposed(by: disposeBag) + } +} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeatureInterface/DetailOnBoardingFactory.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeatureInterface/DetailOnBoardingFactory.swift new file mode 100644 index 00000000..4a3cb3e6 --- /dev/null +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeatureInterface/DetailOnBoardingFactory.swift @@ -0,0 +1,5 @@ +import BaseFeature + +public protocol DetailOnBoardingFactory { + func make() -> BaseViewController +} From 573f4bbec96b7270aa0de28466fe83e08cb87757 Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 11 Dec 2025 16:21:21 +0900 Subject: [PATCH 24/34] =?UTF-8?q?feat/#273:=20=EB=94=94=ED=85=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=98=A8=EB=B3=B4=EB=94=A9=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserDefaultsRepositoryImpl.swift | 18 +++++++++++++++ ...etchVisitDictionaryDetailUseCaseImpl.swift | 23 +++++++++++++++++++ .../Repository/UserDefaultsRepository.swift | 3 +++ .../FetchVisitDictionaryDetailUseCase.swift | 5 ++++ .../DictionaryDetailBaseViewController.swift | 19 ++++++++++++++- 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchVisitDictionaryDetailUseCaseImpl.swift create mode 100644 MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchVisitDictionaryDetailUseCase.swift diff --git a/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift index fe5279ae..30d8a9cf 100644 --- a/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift @@ -7,6 +7,7 @@ public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { private let recentSearchkey = "recentSearch" private let platformKey = "platformKey" private let bookmarkkey = "bookmark" + private let dictionaryDetailkey = "dictionaryDetailkey" public init() {} @@ -85,4 +86,21 @@ public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { return Disposables.create() } } + + public func fetchDictionaryDetail() -> Observable { + return Observable.create { observer in + let hasVisited = UserDefaults.standard.bool(forKey: self.dictionaryDetailkey) + observer.onNext(hasVisited) + observer.onCompleted() + return Disposables.create() + } + } + + public func saveDictionaryDetail() -> Completable { + return Completable.create { completable in + UserDefaults.standard.set(true, forKey: self.dictionaryDetailkey) + completable(.completed) + return Disposables.create() + } + } } diff --git a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchVisitDictionaryDetailUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchVisitDictionaryDetailUseCaseImpl.swift new file mode 100644 index 00000000..72ec9a53 --- /dev/null +++ b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchVisitDictionaryDetailUseCaseImpl.swift @@ -0,0 +1,23 @@ +import DomainInterface +import Foundation + +import RxSwift + +public class FetchVisitDictionaryDetailUseCaseImpl: FetchVisitDictionaryDetailUseCase { + var repository: UserDefaultsRepository + public init(repository: UserDefaultsRepository) { + self.repository = repository + } + + public func execute() -> Observable { + return repository.fetchDictionaryDetail() + .flatMap { hasVisited -> Observable in + if hasVisited { + return .just(true) + } else { + return self.repository.saveDictionaryDetail() + .andThen(.just(false)) + } + } + } +} diff --git a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift index af039ac1..8662e4fe 100644 --- a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift +++ b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift @@ -10,4 +10,7 @@ public protocol UserDefaultsRepository { func fetchBookmark() -> Observable func saveBookmark() -> Completable + + func fetchDictionaryDetail() -> Observable + func saveDictionaryDetail() -> Completable } diff --git a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchVisitDictionaryDetailUseCase.swift b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchVisitDictionaryDetailUseCase.swift new file mode 100644 index 00000000..810c4969 --- /dev/null +++ b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchVisitDictionaryDetailUseCase.swift @@ -0,0 +1,5 @@ +import RxSwift + +public protocol FetchVisitDictionaryDetailUseCase { + func execute() -> Observable +} diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index c076b9b5..37e851a2 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -35,6 +35,8 @@ class DictionaryDetailBaseViewController: BaseViewController { public let dictionaryDetailFactory: DictionaryDetailFactory private let detailOnBoardingFactory: DetailOnBoardingFactory private let appCoordinator: AppCoordinatorProtocol + + private let fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase // MARK: - Components public var mainView = DictionaryDetailBaseView() @@ -42,13 +44,14 @@ class DictionaryDetailBaseViewController: BaseViewController { // 타입설정 public var type: DictionaryItemType - public init(type: DictionaryItemType, bookmarkModalFactory: BookmarkModalFactory, loginFactory: LoginFactory, dictionaryDetailFactory: DictionaryDetailFactory, detailOnBoardingFactory: DetailOnBoardingFactory, appCoordinator: AppCoordinatorProtocol, bookmarkRelay: PublishRelay<(Int, Bool)>?) { + public init(type: DictionaryItemType, bookmarkModalFactory: BookmarkModalFactory, loginFactory: LoginFactory, dictionaryDetailFactory: DictionaryDetailFactory, detailOnBoardingFactory: DetailOnBoardingFactory, appCoordinator: AppCoordinatorProtocol, fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase, bookmarkRelay: PublishRelay<(Int, Bool)>?) { self.type = type self.bookmarkModalFactory = bookmarkModalFactory self.loginFactory = loginFactory self.appCoordinator = appCoordinator self.dictionaryDetailFactory = dictionaryDetailFactory self.detailOnBoardingFactory = detailOnBoardingFactory + self.fetchVisitBookmarkUseCase = fetchVisitBookmarkUseCase mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) self.bookmarkRelay = bookmarkRelay super.init() @@ -335,6 +338,20 @@ extension DictionaryDetailBaseViewController { } } } + + func checkVisited() { + fetchVisitBookmarkUseCase.execute() + .withUnretained(self) + .subscribe{ owner, isVisit in + if !isVisit { + let viewController = owner.detailOnBoardingFactory.make() + viewController.modalPresentationStyle = .overFullScreen + viewController.modalTransitionStyle = .crossDissolve + owner.present(viewController, animated: true) + } + } + .disposed(by: disposeBag) + } } private extension DictionaryDetailBaseViewController { From 942322543553fd4f9edf0f893abf3380f2eaf4f3 Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 11 Dec 2025 17:53:26 +0900 Subject: [PATCH 25/34] =?UTF-8?q?feat/#273:=20=EC=95=BD=EA=B4=80=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MLS/MLS/Application/AppDelegate.swift | 8 + MLS/MLS/Resource/PrivacyPolicy.txt | 141 ++++++++++++++++++ MLS/MLS/Resource/TermsOfService.txt | 48 ++++++ .../DictionaryDetailBaseViewController.swift | 13 +- .../DictionaryDetailFactoryImpl.swift | 15 +- .../CustomerSupportBaseViewController.swift | 31 +++- .../CustomerSupportBaseViewFactoryImpl.swift | 14 +- .../Policy/PolicyFactoryImpl.swift | 12 ++ .../CustomerSupport/Policy/PolicyView.swift | 66 ++++++++ .../Policy/PolicyViewController.swift | 61 ++++++++ .../Terms/TermsDetailReactor.swift | 39 ----- .../Terms/TermsViewController.swift | 11 +- .../PolicyFactory.swift | 5 + .../MyPageFeatureInterface/PolicyType.swift | 35 +++++ 14 files changed, 436 insertions(+), 63 deletions(-) create mode 100644 MLS/MLS/Resource/PrivacyPolicy.txt create mode 100644 MLS/MLS/Resource/TermsOfService.txt create mode 100644 MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyFactoryImpl.swift create mode 100644 MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyView.swift create mode 100644 MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyViewController.swift delete mode 100644 MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsDetailReactor.swift create mode 100644 MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyFactory.swift create mode 100644 MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyType.swift diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index 38e26204..dbd77f20 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -790,6 +790,9 @@ extension AppDelegate { ), setBookmarkUseCase: DIContainer.resolve( type: SetBookmarkUseCase.self + ), + fetchVisitBookmarkUseCase: DIContainer.resolve( + type: FetchVisitBookmarkUseCase.self ) ) } @@ -1157,6 +1160,8 @@ extension AppDelegate { } DIContainer.register(type: CustomerSupportFactory.self) { CustomerSupportBaseViewFactoryImpl( + policyFactory: DIContainer.resolve( + type: PolicyFactory.self), fetchNoticesUseCase: DIContainer.resolve( type: FetchNoticesUseCase.self ), @@ -1218,5 +1223,8 @@ extension AppDelegate { DIContainer.register(type: DetailOnBoardingFactory.self) { DetailOnBoardingFactoryImpl() } + DIContainer.register(type: PolicyFactory.self) { + PolicyFactoryImpl() + } } } diff --git a/MLS/MLS/Resource/PrivacyPolicy.txt b/MLS/MLS/Resource/PrivacyPolicy.txt new file mode 100644 index 00000000..ab6f5bcd --- /dev/null +++ b/MLS/MLS/Resource/PrivacyPolicy.txt @@ -0,0 +1,141 @@ +**제1조(목적)** + +메랜사-메이플랜드사전(이하 "회사")는 회사가 제공하는 서비스(이하 "서비스")를 이용하는 이용자의 개인정보를 보호하고, 관련 법령에 따라 이용자의 권익을 보호하기 위해 본 개인정보처리방침(이하 "본 방침")을 수립합니다. + +**제2조(기본 원칙)** + +회사는 「개인정보 보호법」, 「정보통신망 이용촉진 및 정보보호 등에 관한 법률」 등 관련 법령을 준수하며, 수집한 개인정보는 기능 구현을 위한 목적 내에서만 사용되며, 마케팅이나 영리 목적에는 사용되지 않습니다.회사는 메이플랜드와 공식적 제휴 관계가 없으며, 메이플랜드 관련 정보는 사용자 편의를 위한 보조적 기능으로 제공됩니다. + +**제3조(개인정보의 수집 항목 및 수집 방법)** + +1. 회사는 서비스 제공을 위해 다음과 같은 정보를 수집합니다. + - 필수 수집 항목: 이메일 주소 + - 선택 수집 항목: 게임 캐릭터 관련 정보(레벨, 직업, 스탯) +2. 수집 방법 + - 이용자가 직접 입력한 정보 + - 모바일 기기 식별자 + +**제4조(개인정보의 수집 및 이용 목적)**회사는 수집한 개인정보를 다음의 목적 범위 내에서 이용합니다. + +1. 맞춤형 콘텐츠(아이템/퀘스트/몬스터 추천 등) 제공 +2. 신규 이벤트 알림 등 게임 관련 정보 제공 (단, 영리 목적 아님) +3. 사용자 문의 대응 및 서비스 품질 향 +4. 비정상적 서비스 이용 방지 및 안정적 운영 + +**제5조(보유 및 이용기간)** + +1. 회사는 개인정보 수집 및 이용 목적이 달성된 후에는 지체 없이 해당 정보를 파기합니다. +2. 단, 관련 법령에 따라 일정 기간 동안 보존이 필요한 정보는 법령이 정한 기간 동안 보관됩니다. +3. 내부 방침에 의해 서비스 부정이용기록은 부정 가입 및 이 용 방지를 위하여 회원 탈퇴 시점으로부터 최대 1년간 보관합니다. + +**제6조(개인정보의 이용)** + +회사는 개인정보를 다음 각 호의 경우에 이용합니다. + +- 공지사항의 전달 등 회사운영에 필요한 경우 +- 이용문의에 대한 회신, 불만의 처리 등 이용자에 대한 서비스 개선을 위한 경우 +- 회사의 서비스를 제공하기 위한 경우 +- 법령 및 회사 약관을 위반하는 회원에 대한 이용 제한 조치, 부정 이용 행위를 포함하여 서비스의 원활한 운영에 지장을 주는 행위에 대한 방지 및 제재를 위한 경우 +- 인구통계학적 분석, 서비스 방문 및 이용기록의 분석을 위한 경우 + +**제7조(광고성 정보의 전송)** + +회사는 현재 영리 목적의 광고성 정보를 전송하지 않습니다. 신규 이벤트 알림은 게임 정보성으로서 영리 목적이 아니며, 사용자의 요청에 따라 수신 여부를 설정할 수 있습니다. 향후 영리 목적의 광고성 정보 전송 필요시 별도의 명시적 사전 동의를 받습니다. + +**제8조(개인정보의 제3자 제공 및 위탁)** + +회사는 이용자의 개인정보를 원칙적으로 외부에 제공하지 않으며, 법령에 따라 예외적으로 제공하는 경우를 제외하고는 사전 동의를 받습니다. 현재 어떠한 위탁이나 제3자 제공도 이루어지지 않습니다. + +**제9조(개인정보의 파기원칙)** + +회사는 원칙적으로 이용자의 개인정보 처리 목적의 달성, 보유•이용기간의 경과 등 개인정보 가 필요하지 않을 경우에는 해당 정보를 지체 없이 파기합니다. + +**제10조(개인정보파기절차)** + +이용자가 회원가입 등을 위해 입력한 정보는 개인정보 처리 목적이 달성된 후 별도의 DB로 옮겨져(종이의 경우 별도의 서류함) 내부 방침 및 기타 관련 법령에 의한 정보 보호 사유에 따라(보유 및 이용기간 참조) 일정 기간 저장된 후 파기 되어집니다. + +회사는 파기 사유가 발생한 개인정보를 개인정보보호 책임자의 승인절차를 거쳐 파기 합니다. + +**제11조(개인정보파기방법)** + +회사는 전자적 파일형태로 저장된 개인정보는 기록을 재생할 수 없는 기술적 방법을 사용하여 삭제하며, 종이로 출력된 개인정보는 분쇄기로 분쇄하거나 소각 등을 통하여 파기합니다. + +**제12조(광고성 정보의 전송 조치)** + +1. 회사는 전자적 전송매체를 이용하여 영리목적의 광고성 정보를 전송하는 경우 이용자 의 명시적인 사전동의를 받습니다. 다만, 다음 각호 어느 하나에 해당하는 경우에는 사 전 동의를 받지 않습니다 +2. 회사는 전항에도 불구하고 수신자가 수신거부의사를 표시하거나 사전 동의를 철회한 경우에는 영리목적의 광고성 정보를 전송하지 않으며 수신거부 및 수신동의 철회에 대 한 처리 결과를 알립니다. +3. 회사는 오후 9시부터 그다음 날 오전 8시까지의 시간에 전자적 전송매체를 이용하여 영리목적의 광고성 정보를 전송하는 경우에는 제1항에도 불구하고 그 수신자로부터 별도의 사전 동의를 받습니다. +4. 회사는 전자적 전송매체를 이용하여 영리목적의 광고성 정보를 전송하는 경우 다음의 사항 등을 광고성 정보에 구체적으로 밝힙니다. + - 회사명 및 연락처 + - 수신 거부 또는 수신 동의의 철회 의사표시에 관한 사항의 표시 +5. 회사는 전자적 전송매체를 이용하여 영리목적의 광고성 정보를 전송하는 경우 다음 각 호의 어느 하나에 해당하는 조치를 하지 않습니다. + - 광고성 정보 수신자의 수신거부 또는 수신동의의 철회를 회피방해하는 조치 + - 숫자부호 또는 문자를 조합하여 전화번호•전자우편주소 등 수신자의 연락처를 자동으로 만들어 내는 조치 + - 영리목적의 광고성 정보를 전송할 목적으로 전화번호 또는 전자우편주소를 자동으로 등록하는 조치 + - 광고성 정보 전송자의 신원이나 광고 전송 출처를 감추기 위한 각종 조치 + - 영리목적의 광고성 정보를 전송할 목적으로 수신자를 기망하여 회신을 유도하는각종 조치 + +**제14조(아동의 개인정보보호)** + +1. 회사는 만 14세 미만 아동의 개인정보 보호를 위하여 만 14세 이상의 이용자에 한하여 회원가입을 허용합니다. + +**제15조(개인정보 정보변경 등)** + +1. 이용자는 회사에게 전조의 방법을 통해 개인정보의 오류에 대한 정정을 요청할 수 있 습니다. +2. 회사는 전항의 경우에 개인정보의 정정을 완료하기 전까지 개인정보를 이용 또는 제공 하지 않으며 잘못된 개인정보를 제3자에게 이미 제공한 경우에는 정정 처리결과를 제3 자에게 지체 없이 통지하여 정정이 이루어지도록 하겠습니다. + +**제16조(이용자의 의무)** + +1. 이용자는 자신의 개인정보를 최신의 상태로 유지해야 하며, 이용자의 부정확한 정보 입력으로 발생하는 문제의 책임은 이용자 자신에게 있습니다. +2. 타인의 개인정보를 도용한 회원가입의 경우 이용자 자격을 상실하거나 관련 개인정보 보호 법령에 의해 처벌받을 수 있습니다. +3. 이용자는 전자우편주소, 비밀번호 등에 대한 보안을 유지할 책임이 있으며 제3자에게 이를 양도하거나 대여할 수 없습니다. + +**제17조(개인정보 유출 등에 대한 조치)** + +회사는 개인정보의 분실•도난•유출(이하 "유술 등"이라 한다) 사실을 안 때에는 지체 없이 다 음 각 호의 모든 사항을 해당 이용자에게 알리고 방송통신위원회 또는 한국인터넷진흥원에 신고합니다. + +1. 유출 등이 된 개인정보 항목 +2. 유출 등이 발생한 시점 +3. 이용자가 취할 수 있는 조치 +4. 정보통신서비스 제공자 등의 대응 조치 +5. 이용자가 상담 등을 접수할 수 있는 부서 및 연락처 + +**제18조(개인정보 유출 등에 대한 조치의 예외)** + +회사는 전조에도 불구하고 이용자의 연락처를 알 수 없는 등 정당한 사유가 있는 경우에는 회 사의 홈페이지에 30일 이상 게시하는 방법으로 전조의 통지를 갈음하는 조치를 취할 수 있습 니다. + +**제19조(국외 이전 개인정보의 보호)** + +1. 회사는 이용자의 개인정보에 관하여 개인정보보호법 등 관계 법규를 위반하는 사항을 내용으로 하는 국제계약을 체결하지 않습니다. +2. 회사는 이용자의 개인정보를 국외에 제공(조회되는 경우를 포함) • 처리위탁 보관(이 하"이전"이라 함)하려면 이용자의 동의를 받습니다. 다만, 본조 제3항 각 호의 사항 모 두를 개인정보보호법 등 관계 법규에 따라 공개하거나 전자우편 등 대통령령으로 정하 는 방법에 따라 이용자에게 알린 경우에는 개인정보 처리위탁 • 보관에 따른 동의절차 를 거치지 아니할 수 있습니다. +3. 회사는 본조 제2항 본문에 따른 동의를 받으려면 미리 다음 각 호의 사항 모두를 이용 자에게 고지합니다. + - 이전되는 개인정보 항목 + - 개인정보가 이전되는 국가, 이전일시 및 이전방법 + - 개인정보를 이전받는 자의 성명(법인인 경우 그 명칭 및 정보관리 책임자의 연락처를 말한다 + - 개인정보를 이전받는 자의 개인정보 이용목적 및 보유 이용 기간 +4. 회사는 본조 제2항 본문에 따른 동의를 받아 개인정보를 국외로 이전하는 경우 개인정 보보호법 대통령령 등 관계법규에서 정하는 바에 따라 보호조치를 합니다. + +**제20조(개인정보 자동 수집 장치의 설치•운영 및 거부에 관한 사항)** + +회사는 이용자에게 개별적인 맞춤서비스를 제공하고 서비스 이용을 분석하기 위해 모바일 기기 식별자를 사용합니다. + +회사는 이용자에게 안정적인 서비스를 제공하고 서비스 이용 행태를 분석하기 위해 모바일 기기 식별자(예: UUID 등)를 수집·이용합니다. + +수집된 정보는 기기 기반 사용자 구분, 서비스 품질 개선, 오류 분석 및 부정 사용 방지를 위한 목적으로만 사용되며, 광고 식별 및 마케팅 목적으로는 이용되지 않습니다. + +이용자는 앱 삭제 또는 회사에 별도 요청을 통해 모바일 기기 식별자의 수집 및 이용에 대한 동의를 철회할 수 있습니다. 동의 철회를 원하실 경우, [mapleland2024@gmail.com]로 문의해 주시기 바랍니다. + +**제21조(권익침해에 대한 구제방법)** + +1. 정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국 인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고, 상담에 대하여는 아래의 기관에 문의하시기 바랍니다. + - 개인정보분쟁조정위원회 : (국번없이) 1833-6972 (www.kopico.go.kr) + - 개인정보침해신고센터 : (국번없이) 118(privacy.kisa.or.kr) + - 대검찰청 : (국번없이) 1301 (www.spo.go.kr) + - 경찰청 : (국번없이) 182 (ecrm.cyber.go.kr) +2. 회사는 정보주체의 개인정보자기결정권을 보장하고, 개인정보침해로 인한 상담 및 피 해 구제를 위해 노력하고 있으며, 신고나 상담이 필요한 경우 제1항의 담당부서로 연락 해주시기 바랍니다. +3. 개인정보 보호법 제35조(개인정보의 열량), 제36조(개인정보의 정정삭제), 제37조)개인정보의 처리정지 등)의 규정에 의한 요구에 대 하여 공공기관의 장이 행한 처분 또 는 부작위로 인하여 권리 또는 이익의 침해를 받은 자는 행정심판법이 정하는 바에 따 라 행정심판을 청구할 수 있습니다. + - 중앙행정심판위원회 : (국번없이) 110 (www.simpan.go.kr) + +부칙 + +제1조 본 방침은 2025.12.12.부터 시행됩니다. diff --git a/MLS/MLS/Resource/TermsOfService.txt b/MLS/MLS/Resource/TermsOfService.txt new file mode 100644 index 00000000..36b9196b --- /dev/null +++ b/MLS/MLS/Resource/TermsOfService.txt @@ -0,0 +1,48 @@ +제1조 (목적) + +본 약관은 메랜사(이하 '회사')가 제공하는 메랜사-메이플랜드사전 서비스(이하 '서비스') 이용과 관련하여 회사와 회원 간의 권리, 의무 및 책임사항과 기타 필요한 사항을 규정하는 것을 목적으로 합니다. + +제2조 (정의) + +본 약관에서 사용하는 용어의 정의는 다음과 같습니다. + +1. '서비스'라 함은 회사가 제공하는 모바일 어플리케이션 및 그와 관련된 제반 서비스를 의미합니다. +2. '이용자'란 본 약관에 따라 회사의 서비스를 이용하는 모든 개인을 의미합니다. +3. '회원'이란 회사에 개인정보를 제공하고 서비스 가입 절차를 완료한 자를 말합니다. +4. '비회원'이란 회원 가입 절차 없이 회사가 제공하는 일부 서비스를 이용하는 자를 말합니다. + +제3조 (약관의 효력 및 변경) + +1. 본 약관은 회사가 제공하는 서비스에 게시하거나 기타의 방법으로 공지함으로써 효력이 발생합니다. +2. 회사는 약관 변경 시 7일 전에 공지하며, 이용자에게 불리한 중대한 변경이 있을 경우 최소 30일 전에 공지합니다. + +제4조 (서비스의 제공) + +1. 회사는 메이플랜드 유저를 위한 아이템, 퀘스트, 몬스터 추천, 신규 이벤트 정보 알림 및 즐겨찾기 등의 비영리 목적의 게임 보조 서비스를 제공합니다. +2. 회사는 서비스 품질 향상 및 이용자의 편의를 위해 이용자의 캐릭터 레벨, 스탯 정보 및 서비스 이용 중 발생하는 클릭 이력, 조회수, 검색 기록 등을 수집하여 보관할 수 있으며, 이는 이용자에게 더욱 정교하고 개인화된 추천 기능을 제공하기 위한 목적입니다. 수집된 데이터는 마케팅이나 기타 영리적 목적으로 사용되지 않습니다. +3. 서비스는 연중무휴 제공을 원칙으로 하나, 기술적 또는 운영상의 이유로 서비스가 일시 중단될 수 있습니다. + +제5조 (회원가입 및 승낙) + +1. 회사는 이용자의 회원 가입 신청에 특별한 사정이 없는 한 이를 승낙합니다. +2. 회사는 기술적 문제, 허위정보 기재 등의 이유로 회원가입 신청을 승낙하지 않을 수 있습니다. + +제6조 (회원의 의무) + +1. 회원은 본인의 개인정보를 최신 상태로 유지해야 합니다. +2. 회원은 서비스 이용 중 타인의 정보를 도용하거나 부정한 방법으로 서비스를 이용해서는 안 됩니다. + +제7조 (회사의 의무) + +1. 회사는 안정적인 서비스 제공을 위해 노력하며, 개인정보 보호를 위한 적절한 조치를 취합니다. +2. 회사는 개인정보처리방침을 공개하여 회원의 개인정보가 어떻게 처리되는지 투명하게 안내합니다. + +제8조 (개인정보보호) 회사는 회원의 개인정보 보호를 위해 관련 법령을 준수하며, 개인정보처리방침을 별도로 운영합니다. + +제9조 (서비스 이용제한) 회사는 이용자가 본 약관을 위반하거나 서비스 운영에 지장을 주는 행위를 할 경우 서비스 이용을 제한할 수 있습니다. + +제10조 (책임의 제한) 회사는 천재지변, 전쟁 등 불가항력적 사유 또는 회사의 고의 또는 중대한 과실 없이 발생한 서비스 장애 및 이용자의 손해에 대해서는 책임을 지지 않습니다. + +제11조 (분쟁의 해결) 본 약관과 관련된 분쟁 발생 시 회사와 회원은 원만한 해결을 위해 노력하며, 해결되지 않을 경우 관련 법령에 따라 처리합니다. + +부칙 본 약관은 2025년 12월 12일부터 시행됩니다. diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index 37e851a2..f0c9c994 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -35,7 +35,7 @@ class DictionaryDetailBaseViewController: BaseViewController { public let dictionaryDetailFactory: DictionaryDetailFactory private let detailOnBoardingFactory: DetailOnBoardingFactory private let appCoordinator: AppCoordinatorProtocol - + private let fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase // MARK: - Components @@ -44,7 +44,16 @@ class DictionaryDetailBaseViewController: BaseViewController { // 타입설정 public var type: DictionaryItemType - public init(type: DictionaryItemType, bookmarkModalFactory: BookmarkModalFactory, loginFactory: LoginFactory, dictionaryDetailFactory: DictionaryDetailFactory, detailOnBoardingFactory: DetailOnBoardingFactory, appCoordinator: AppCoordinatorProtocol, fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase, bookmarkRelay: PublishRelay<(Int, Bool)>?) { + public init( + type: DictionaryItemType, + bookmarkModalFactory: BookmarkModalFactory, + loginFactory: LoginFactory, + dictionaryDetailFactory: DictionaryDetailFactory, + detailOnBoardingFactory: DetailOnBoardingFactory, + appCoordinator: AppCoordinatorProtocol, + fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase, + bookmarkRelay: PublishRelay<(Int, Bool)>? + ) { self.type = type self.bookmarkModalFactory = bookmarkModalFactory self.loginFactory = loginFactory diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift index a21540e0..56933afc 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift @@ -29,6 +29,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { private let dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase private let checkLoginUseCase: CheckLoginUseCase private let setBookmarkUseCase: SetBookmarkUseCase + private let fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase public init( loginFactory: @escaping () -> LoginFactory, @@ -50,7 +51,8 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { dictionaryDetailMonsterDropItemUseCase: FetchDictionaryDetailMonsterItemsUseCase, dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase, checkLoginUseCase: CheckLoginUseCase, - setBookmarkUseCase: SetBookmarkUseCase + setBookmarkUseCase: SetBookmarkUseCase, + fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase ) { self.loginFactory = loginFactory self.bookmarkModalFactory = bookmarkModalFactory @@ -72,6 +74,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { self.setBookmarkUseCase = setBookmarkUseCase self.appCoordinator = appCoordinator self.dictionaryDetailFactory = dictionaryDetailFactory + self.fetchVisitBookmarkUseCase = fetchVisitBookmarkUseCase } public func make(type: DictionaryType, id: Int, bookmarkRelay: PublishRelay<(Int, Bool)>?) -> BaseViewController { @@ -88,7 +91,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), + appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, bookmarkRelay: bookmarkRelay ) let reactor = ItemDictionaryDetailReactor( @@ -108,7 +111,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), + appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, bookmarkRelay: bookmarkRelay ) let reactor = MonsterDictionaryDetailReactor( @@ -129,7 +132,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), + appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, bookmarkRelay: bookmarkRelay ) let reactor = MapDictionaryDetailReactor( @@ -150,7 +153,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), + appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, bookmarkRelay: bookmarkRelay ) let reactor = NpcDictionaryDetailReactor( @@ -171,7 +174,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), + appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, bookmarkRelay: bookmarkRelay ) let reactor = QuestDictionaryDetailReactor( diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift index 8c89045d..ea446b86 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift @@ -3,13 +3,14 @@ import UIKit import BaseFeature import DesignSystem import DomainInterface +import MyPageFeatureInterface import RxCocoa import RxGesture import RxSwift /* -**부모 뷰컨이 될 것 같음** - */ + **부모 뷰컨이 될 것 같음** + */ class CustomerSupportBaseViewController: BaseViewController { // MARK: - Properties public var disposeBag = DisposeBag() @@ -19,13 +20,23 @@ class CustomerSupportBaseViewController: BaseViewController { public var urlStrings: [String] = [] var onItemTapped: ((Int) -> Void)? + private let policyFactory: PolicyFactory? + // MARK: - Components public var mainView = CustomerSupportBaseView() public var type: CustomerSupportType public init(type: CustomerSupportType) { + self.type = type + self.policyFactory = nil + super.init() + mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) + } + + public init(type: CustomerSupportType, policyFactory: PolicyFactory) { self.type = type mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) + self.policyFactory = policyFactory super.init() } @@ -56,7 +67,6 @@ class CustomerSupportBaseViewController: BaseViewController { self?.handleItemTap(index: index) }) .disposed(by: disposeBag) - } } @@ -90,8 +100,8 @@ extension CustomerSupportBaseViewController { } } } -extension CustomerSupportBaseViewController { +extension CustomerSupportBaseViewController { func handleItemTap(index: Int) { // 원하는 URL 열기 또는 네비게이션 처리 switch type { @@ -100,11 +110,18 @@ extension CustomerSupportBaseViewController { guard index < urlStrings.count else { return } let url = urlStrings[index] let webViewController = WebViewController(urlString: url) -// navigationController?.pushViewController(webViewController, animated: true) present(webViewController, animated: true) case .terms: - let viewController = TermsDetailViewController() - navigationController?.pushViewController(viewController, animated: true) + switch index { + case 0: + guard let viewController = policyFactory?.make(type: .service) else { return } + navigationController?.pushViewController(viewController, animated: true) + case 1: + guard let viewController = policyFactory?.make(type: .service) else { return } + navigationController?.pushViewController(viewController, animated: true) + default: + break + } } } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewFactoryImpl.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewFactoryImpl.swift index 485b2bdd..2a09be91 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewFactoryImpl.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewFactoryImpl.swift @@ -3,13 +3,23 @@ import DomainInterface import MyPageFeatureInterface public final class CustomerSupportBaseViewFactoryImpl: CustomerSupportFactory { + private let policyFactory: PolicyFactory + private let fetchNoticesUseCase: FetchNoticesUseCase private let fetchOngoingEventsUseCase: FetchOngoingEventsUseCase private let fetchOutdatedEventsUseCase: FetchOutdatedEventsUseCase private let fetchPatchNotesUseCase: FetchPatchNotesUseCase private let setReadUseCase: SetReadUseCase - public init(fetchNoticesUseCase: FetchNoticesUseCase, fetchOngoingEventsUseCase: FetchOngoingEventsUseCase, fetchOutdatedEventsUseCase: FetchOutdatedEventsUseCase, fetchPatchNotesUseCase: FetchPatchNotesUseCase, setReadUseCase: SetReadUseCase) { + public init( + policyFactory: PolicyFactory, + fetchNoticesUseCase: FetchNoticesUseCase, + fetchOngoingEventsUseCase: FetchOngoingEventsUseCase, + fetchOutdatedEventsUseCase: FetchOutdatedEventsUseCase, + fetchPatchNotesUseCase: FetchPatchNotesUseCase, + setReadUseCase: SetReadUseCase + ) { + self.policyFactory = policyFactory self.fetchNoticesUseCase = fetchNoticesUseCase self.fetchOngoingEventsUseCase = fetchOngoingEventsUseCase self.fetchOutdatedEventsUseCase = fetchOutdatedEventsUseCase @@ -37,7 +47,7 @@ public final class CustomerSupportBaseViewFactoryImpl: CustomerSupportFactory { viewController.reactor = PatchNoteReactor(fetchPatchNotesUseCase: fetchPatchNotesUseCase, setReadUseCase: setReadUseCase) } case .terms: - viewController = TermsViewController(type: .terms) + viewController = TermsViewController(type: .terms, policyFactory: policyFactory) if let viewController = viewController as? TermsViewController { } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyFactoryImpl.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyFactoryImpl.swift new file mode 100644 index 00000000..dcadf10a --- /dev/null +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyFactoryImpl.swift @@ -0,0 +1,12 @@ +import BaseFeature +import MyPageFeatureInterface + +public final class PolicyFactoryImpl: PolicyFactory { + public init() {} + + public func make(type: PolicyType) -> BaseViewController { + let viewController = PolicyViewController(type: type) + viewController.isBottomTabbarHidden = true + return viewController + } +} diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyView.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyView.swift new file mode 100644 index 00000000..743a3894 --- /dev/null +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyView.swift @@ -0,0 +1,66 @@ +import UIKit + +import DesignSystem +import MyPageFeatureInterface + +final class PolicyView: UIView { + // MARK: - Type + public enum Constant { + static let verticalMargin: CGFloat = 20 + static let horizontalMargin: CGFloat = 16 + } + + // MARK: - Components + public let headerView = NavigationBar(type: .collection("약관 및 정책")) + + private let titleLabel = UILabel() + + private let contentTextView = UITextView() + + // MARK: - Init + init(type: PolicyType) { + super.init(frame: .zero) + addViews() + setupConstraints() + configureUI(type: type) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - SetUp +private extension PolicyView { + func addViews() { + addSubview(headerView) + addSubview(titleLabel) + addSubview(contentTextView) + } + + func setupConstraints() { + headerView.snp.makeConstraints { make in + make.top.horizontalEdges.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(headerView.snp.bottom).offset(Constant.verticalMargin) + make.horizontalEdges.equalToSuperview().inset(Constant.horizontalMargin) + } + + contentTextView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(Constant.verticalMargin) + make.horizontalEdges.equalToSuperview().inset(Constant.horizontalMargin) + make.bottom.equalToSuperview().inset(Constant.verticalMargin) + } + } + + func configureUI(type: PolicyType) { + headerView.editButton.isHidden = true + headerView.addButton.isHidden = true + headerView.setTitle(title: type.title) + titleLabel.attributedText = .makeStyledString(font: .h_xxxl_sb, text: "메랜사 \(type.title)", alignment: .left) + contentTextView.attributedText = .makeStyledString(font: .b_s_r, text: type.content, alignment: .left) + } +} diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyViewController.swift new file mode 100644 index 00000000..bd108f2c --- /dev/null +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/PolicyViewController.swift @@ -0,0 +1,61 @@ +import UIKit + +import BaseFeature +import DesignSystem +import DomainInterface +import MyPageFeatureInterface + +import RxCocoa +import RxGesture +import RxSwift +/* +**부모 뷰컨이 될 것 같음** + */ +class PolicyViewController: BaseViewController { + // MARK: - Properties + public var disposeBag = DisposeBag() + + // MARK: - Components + public var mainView: PolicyView + + public init(type: PolicyType) { + self.mainView = PolicyView(type: type) + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + addViews() + setupConstaraints() + bind() + } +} + +// MARK: - SetUp +extension PolicyViewController { + func addViews() { + view.addSubview(mainView) + } + + func setupConstaraints() { + mainView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.horizontalEdges.bottom.equalToSuperview() + } + } + + func bind() { + mainView.headerView.leftButton.rx.tap + .withUnretained(self) + .observe(on: MainScheduler.instance) + .subscribe { owner, _ in + owner.navigationController?.popViewController(animated: true) + } + .disposed(by: disposeBag) + } +} diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsDetailReactor.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsDetailReactor.swift deleted file mode 100644 index fc2c4053..00000000 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsDetailReactor.swift +++ /dev/null @@ -1,39 +0,0 @@ -import DomainInterface - -import ReactorKit - -public final class TermsDetailReactor: Reactor { - // MARK: - Reactor - public enum Route { - case none - } - - public enum Action { - - } - - public enum Mutation { - - } - - public struct State { - - } - - public var initialState: State - private let disposeBag = DisposeBag() - - public init() { - self.initialState = .init() - } - - public func mutate(action: Action) -> Observable { - return .empty() - } - - public func reduce(state: State, mutation: Mutation) -> State { - var newState = state - - return newState - } -} diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsViewController.swift index 53a8da41..bf93585c 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsViewController.swift @@ -1,19 +1,16 @@ import UIKit +import MyPageFeatureInterface + final class TermsViewController: CustomerSupportBaseViewController { override func viewDidLoad() { super.viewDidLoad() - let items = [ - "서비스 이용약관", - "개인정보 처리방침", - "마케팅 정보 수신 동의", - "오픈소스 라이선스 보기" - ] + let items = PolicyType.allCases mainView.setMenuHidden(true) mainView.changeSetupConstraints() - createTermsDetailItem(items: items) + createTermsDetailItem(items: items.map { $0.title }) } } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyFactory.swift b/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyFactory.swift new file mode 100644 index 00000000..c97ad2ea --- /dev/null +++ b/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyFactory.swift @@ -0,0 +1,5 @@ +import BaseFeature + +public protocol PolicyFactory { + func make(type: PolicyType) -> BaseViewController +} diff --git a/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyType.swift b/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyType.swift new file mode 100644 index 00000000..a2f39c25 --- /dev/null +++ b/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyType.swift @@ -0,0 +1,35 @@ +import Foundation + +public enum PolicyType: CaseIterable { + case service + case privacy + + public var title: String { + switch self { + case .service: + "서비스 이용약관" + case .privacy: + "개인정보 처리방침" + } + } + + public var fileName: String { + switch self { + case .service: + return "TermsOfService.txt" + case .privacy: + return "PrivacyPolicy.txt" + } + } + + public var content: String { + var result = "" + guard let pahts = Bundle.main.path(forResource: fileName, ofType: nil) else { return "" } + do { + result = try String(contentsOfFile: pahts, encoding: .utf8) + return result + } catch { + return "Error: file read failed - \(error.localizedDescription)" + } + } +} From d703859fc9e225e9cb1508259968d98897b3ff6f Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 11 Dec 2025 21:51:24 +0900 Subject: [PATCH 26/34] =?UTF-8?q?feat/#273:=20=EC=98=A4=ED=94=88=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=20=EB=9D=BC=EC=9D=B4=EC=84=A0=EC=8A=A4=20=ED=91=9C?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MLS/MLS.xcodeproj/project.pbxproj | 4 + .../xcshareddata/swiftpm/Package.resolved | 2 +- MLS/MLS/Resource/Settings.bundle/Root.plist | 19 ++ ...om.mono0926.LicensePlist.latest_result.txt | 47 ++++ .../com.mono0926.LicensePlist.plist | 191 +++++++++++++ .../com.mono0926.LicensePlist/Alamofire.plist | 36 +++ .../GoogleAppMeasurement.plist | 219 +++++++++++++++ .../GoogleDataTransport.plist | 219 +++++++++++++++ .../GoogleUtilities.plist | 253 ++++++++++++++++++ .../ReactorKit.plist | 38 +++ .../com.mono0926.LicensePlist/RxGesture.plist | 36 +++ .../RxKeyboard.plist | 38 +++ .../com.mono0926.LicensePlist/RxSwift.plist | 26 ++ .../com.mono0926.LicensePlist/SnapKit.plist | 36 +++ .../WeakMapTable.plist | 38 +++ .../abseil-cpp-binary.plist | 218 +++++++++++++++ .../com.mono0926.LicensePlist/app-check.plist | 219 +++++++++++++++ .../firebase-ios-sdk.plist | 219 +++++++++++++++ ...gle-ads-on-device-conversion-ios-sdk.plist | 219 +++++++++++++++ .../grpc-binary.plist | 218 +++++++++++++++ .../gtm-session-fetcher.plist | 219 +++++++++++++++ .../interop-ios-for-google-sdks.plist | 219 +++++++++++++++ .../kakao-ios-sdk.plist | 218 +++++++++++++++ .../com.mono0926.LicensePlist/leveldb.plist | 44 +++ .../com.mono0926.LicensePlist/nanopb.plist | 37 +++ .../com.mono0926.LicensePlist/promises.plist | 219 +++++++++++++++ .../swift-protobuf.plist | 228 ++++++++++++++++ .../Settings.bundle/en.lproj/Root.strings | Bin 0 -> 546 bytes .../CustomerSupportBaseViewController.swift | 5 + .../TermsViewController.swift | 0 .../Terms/TermsDetailViewController.swift | 9 - .../MyPageFeatureInterface/PolicyType.swift | 9 +- 32 files changed, 3490 insertions(+), 12 deletions(-) create mode 100644 MLS/MLS/Resource/Settings.bundle/Root.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/Alamofire.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleAppMeasurement.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleDataTransport.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleUtilities.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/ReactorKit.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxGesture.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxKeyboard.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxSwift.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/SnapKit.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/WeakMapTable.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/abseil-cpp-binary.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/app-check.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/firebase-ios-sdk.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/google-ads-on-device-conversion-ios-sdk.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/grpc-binary.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/gtm-session-fetcher.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/interop-ios-for-google-sdks.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/kakao-ios-sdk.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/leveldb.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/nanopb.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/promises.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/swift-protobuf.plist create mode 100644 MLS/MLS/Resource/Settings.bundle/en.lproj/Root.strings rename MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/{Terms => Policy}/TermsViewController.swift (100%) delete mode 100644 MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsDetailViewController.swift diff --git a/MLS/MLS.xcodeproj/project.pbxproj b/MLS/MLS.xcodeproj/project.pbxproj index 3ad0da22..f09ba99c 100644 --- a/MLS/MLS.xcodeproj/project.pbxproj +++ b/MLS/MLS.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 779A49102E1AD26D00ABDE4F /* BookmarkFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 779A490F2E1AD26D00ABDE4F /* BookmarkFeatureInterface.framework */; }; 779A49112E1AD26D00ABDE4F /* BookmarkFeatureInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 779A490F2E1AD26D00ABDE4F /* BookmarkFeatureInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 77B1F9952EE06A4E00AE4B4D /* RxGesture in Frameworks */ = {isa = PBXBuildFile; productRef = 77B1F9942EE06A4E00AE4B4D /* RxGesture */; }; + 77E260412EEABEC40059E889 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 77E260402EEABEC40059E889 /* Settings.bundle */; }; 77EB18D62DED9256004FB380 /* AuthFeature.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77EB18D52DED9256004FB380 /* AuthFeature.framework */; }; 77EB18D72DED9256004FB380 /* AuthFeature.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 77EB18D52DED9256004FB380 /* AuthFeature.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -127,6 +128,7 @@ 779A490C2E1AD26700ABDE4F /* BookmarkFeature.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BookmarkFeature.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 779A490F2E1AD26D00ABDE4F /* BookmarkFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BookmarkFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 77BEB0402DBA84B0002FFCFC /* MLSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MLSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 77E260402EEABEC40059E889 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = Settings.bundle; path = MLS/Resource/Settings.bundle; sourceTree = ""; }; 77EB18D52DED9256004FB380 /* AuthFeature.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AuthFeature.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -275,6 +277,7 @@ 087D3EDF2DA7972C002F924D = { isa = PBXGroup; children = ( + 77E260402EEABEC40059E889 /* Settings.bundle */, 77660AD12DD0D361007A4EF3 /* KakaoConfig.xcconfig */, 085A7F742DAF99570046663F /* .swiftlint.yml */, 087D3EEA2DA7972C002F924D /* MLS */, @@ -433,6 +436,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 77E260412EEABEC40059E889 /* Settings.bundle in Resources */, 77660AD22DD0D361007A4EF3 /* KakaoConfig.xcconfig in Resources */, 085A7F752DAF99570046663F /* .swiftlint.yml in Resources */, ); diff --git a/MLS/MLS.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MLS/MLS.xcworkspace/xcshareddata/swiftpm/Package.resolved index c7f6a5b9..386ec176 100644 --- a/MLS/MLS.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/MLS/MLS.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ba8fe7771291f9a80fd693d31ec15a696e49e0d6060bae5c6576060534c1d5b4", + "originHash" : "bfbb1a7b185edd3389821cec79c508d208318809d55699ccbc2cee82e1a49c3d", "pins" : [ { "identity" : "abseil-cpp-binary", diff --git a/MLS/MLS/Resource/Settings.bundle/Root.plist b/MLS/MLS/Resource/Settings.bundle/Root.plist new file mode 100644 index 00000000..56fa0b03 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/Root.plist @@ -0,0 +1,19 @@ + + + + + StringsTable + Root + PreferenceSpecifiers + + + Type + PSChildPaneSpecifier + Title + 오픈소스 라이선스 + File + com.mono0926.LicensePlist + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt new file mode 100644 index 00000000..981a58a2 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist.latest_result.txt @@ -0,0 +1,47 @@ +name: abseil-cpp-binary, nameSpecified: abseil, owner: google, version: 1.2024072200.0, source: https://github.com/google/abseil-cpp-binary + +name: Alamofire, nameSpecified: Alamofire, owner: Alamofire, version: 5.10.2, source: https://github.com/Alamofire/Alamofire + +name: app-check, nameSpecified: AppCheck, owner: google, version: 11.2.0, source: https://github.com/google/app-check + +name: firebase-ios-sdk, nameSpecified: Firebase, owner: firebase, version: 11.15.0, source: https://github.com/firebase/firebase-ios-sdk + +name: google-ads-on-device-conversion-ios-sdk, nameSpecified: GoogleAdsOnDeviceConversion, owner: googleads, version: 2.1.0, source: https://github.com/googleads/google-ads-on-device-conversion-ios-sdk + +name: GoogleAppMeasurement, nameSpecified: GoogleAppMeasurement, owner: google, version: 11.15.0, source: https://github.com/google/GoogleAppMeasurement + +name: GoogleDataTransport, nameSpecified: GoogleDataTransport, owner: google, version: 10.1.0, source: https://github.com/google/GoogleDataTransport + +name: GoogleUtilities, nameSpecified: GoogleUtilities, owner: google, version: 8.1.0, source: https://github.com/google/GoogleUtilities + +name: grpc-binary, nameSpecified: gRPC, owner: google, version: 1.69.0, source: https://github.com/google/grpc-binary + +name: gtm-session-fetcher, nameSpecified: gtm-session-fetcher, owner: google, version: 4.5.0, source: https://github.com/google/gtm-session-fetcher + +name: interop-ios-for-google-sdks, nameSpecified: InteropForGoogle, owner: google, version: 101.0.0, source: https://github.com/google/interop-ios-for-google-sdks + +name: kakao-ios-sdk, nameSpecified: kakao-ios-sdk, owner: kakao, version: , source: https://github.com/kakao/kakao-ios-sdk + +name: leveldb, nameSpecified: leveldb, owner: firebase, version: 1.22.5, source: https://github.com/firebase/leveldb + +name: nanopb, nameSpecified: nanopb, owner: firebase, version: 2.30910.0, source: https://github.com/firebase/nanopb + +name: promises, nameSpecified: Promises, owner: google, version: 2.4.0, source: https://github.com/google/promises + +name: ReactorKit, nameSpecified: ReactorKit, owner: ReactorKit, version: 3.2.0, source: https://github.com/ReactorKit/ReactorKit + +name: RxGesture, nameSpecified: RxGesture, owner: RxSwiftCommunity, version: 4.0.4, source: https://github.com/RxSwiftCommunity/RxGesture + +name: RxKeyboard, nameSpecified: RxKeyboard, owner: RxSwiftCommunity, version: 2.0.1, source: https://github.com/RxSwiftCommunity/RxKeyboard + +name: RxSwift, nameSpecified: RxSwift, owner: ReactiveX, version: 6.9.1, source: https://github.com/ReactiveX/RxSwift + +name: SnapKit, nameSpecified: SnapKit, owner: SnapKit, version: 5.7.1, source: https://github.com/SnapKit/SnapKit + +name: swift-protobuf, nameSpecified: SwiftProtobuf, owner: apple, version: 1.30.0, source: https://github.com/apple/swift-protobuf + +name: WeakMapTable, nameSpecified: WeakMapTable, owner: ReactorKit, version: 1.2.1, source: https://github.com/ReactorKit/WeakMapTable + +add-version-numbers: false + +LicensePlist Version: 3.27.2 \ No newline at end of file diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist.plist new file mode 100644 index 00000000..eb0b87e9 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist.plist @@ -0,0 +1,191 @@ + + + + + PreferenceSpecifiers + + + Title + Licenses + Type + PSGroupSpecifier + + + File + com.mono0926.LicensePlist/abseil-cpp-binary + Title + abseil + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/Alamofire + Title + Alamofire + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/app-check + Title + AppCheck + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/firebase-ios-sdk + Title + Firebase + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/google-ads-on-device-conversion-ios-sdk + Title + GoogleAdsOnDeviceConversion + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/GoogleAppMeasurement + Title + GoogleAppMeasurement + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/GoogleDataTransport + Title + GoogleDataTransport + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/GoogleUtilities + Title + GoogleUtilities + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/grpc-binary + Title + gRPC + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/gtm-session-fetcher + Title + gtm-session-fetcher + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/interop-ios-for-google-sdks + Title + InteropForGoogle + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/kakao-ios-sdk + Title + kakao-ios-sdk + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/leveldb + Title + leveldb + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/nanopb + Title + nanopb + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/promises + Title + Promises + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/ReactorKit + Title + ReactorKit + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/RxGesture + Title + RxGesture + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/RxKeyboard + Title + RxKeyboard + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/RxSwift + Title + RxSwift + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/SnapKit + Title + SnapKit + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/swift-protobuf + Title + SwiftProtobuf + Type + PSChildPaneSpecifier + + + File + com.mono0926.LicensePlist/WeakMapTable + Title + WeakMapTable + Type + PSChildPaneSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/Alamofire.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/Alamofire.plist new file mode 100644 index 00000000..d0e2be43 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/Alamofire.plist @@ -0,0 +1,36 @@ + + + + + PreferenceSpecifiers + + + FooterText + Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleAppMeasurement.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleAppMeasurement.plist new file mode 100644 index 00000000..9a823443 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleAppMeasurement.plist @@ -0,0 +1,219 @@ + + + + + PreferenceSpecifiers + + + FooterText + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleDataTransport.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleDataTransport.plist new file mode 100644 index 00000000..9a823443 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleDataTransport.plist @@ -0,0 +1,219 @@ + + + + + PreferenceSpecifiers + + + FooterText + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleUtilities.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleUtilities.plist new file mode 100644 index 00000000..eadbcb1f --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/GoogleUtilities.plist @@ -0,0 +1,253 @@ + + + + + PreferenceSpecifiers + + + FooterText + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + License + Apache-2.0 + Type + PSGroupSpecifier + + + FooterText + ---------------------------------------- + License + Apache-2.0 + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2017 Landon J. Fuller <landon@landonf.org> +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/ReactorKit.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/ReactorKit.plist new file mode 100644 index 00000000..3a8bd5b8 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/ReactorKit.plist @@ -0,0 +1,38 @@ + + + + + PreferenceSpecifiers + + + FooterText + The MIT License (MIT) + +Copyright (c) 2017 Suyeol Jeon (xoul.kr) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + License + MIT + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxGesture.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxGesture.plist new file mode 100644 index 00000000..c51c983b --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxGesture.plist @@ -0,0 +1,36 @@ + + + + + PreferenceSpecifiers + + + FooterText + Copyright (c) RxSwiftCommunity + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxKeyboard.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxKeyboard.plist new file mode 100644 index 00000000..3b0bedba --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxKeyboard.plist @@ -0,0 +1,38 @@ + + + + + PreferenceSpecifiers + + + FooterText + The MIT License (MIT) + +Copyright (c) 2016 Suyeol Jeon (xoul.kr) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + License + MIT + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxSwift.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxSwift.plist new file mode 100644 index 00000000..147faea5 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/RxSwift.plist @@ -0,0 +1,26 @@ + + + + + PreferenceSpecifiers + + + FooterText + **The MIT License** +**Copyright © 2015 Shai Mishali, Krunoslav Zaher** +**All rights reserved.** + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + License + MIT + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/SnapKit.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/SnapKit.plist new file mode 100644 index 00000000..a32378a5 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/SnapKit.plist @@ -0,0 +1,36 @@ + + + + + PreferenceSpecifiers + + + FooterText + Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/WeakMapTable.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/WeakMapTable.plist new file mode 100644 index 00000000..aa54172d --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/WeakMapTable.plist @@ -0,0 +1,38 @@ + + + + + PreferenceSpecifiers + + + FooterText + The MIT License (MIT) + +Copyright (c) 2020 Suyeol Jeon (xoul.kr) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + License + MIT + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/abseil-cpp-binary.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/abseil-cpp-binary.plist new file mode 100644 index 00000000..58aee5cc --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/abseil-cpp-binary.plist @@ -0,0 +1,218 @@ + + + + + PreferenceSpecifiers + + + FooterText + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/app-check.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/app-check.plist new file mode 100644 index 00000000..9a823443 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/app-check.plist @@ -0,0 +1,219 @@ + + + + + PreferenceSpecifiers + + + FooterText + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/firebase-ios-sdk.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/firebase-ios-sdk.plist new file mode 100644 index 00000000..9a823443 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/firebase-ios-sdk.plist @@ -0,0 +1,219 @@ + + + + + PreferenceSpecifiers + + + FooterText + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/google-ads-on-device-conversion-ios-sdk.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/google-ads-on-device-conversion-ios-sdk.plist new file mode 100644 index 00000000..9a823443 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/google-ads-on-device-conversion-ios-sdk.plist @@ -0,0 +1,219 @@ + + + + + PreferenceSpecifiers + + + FooterText + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/grpc-binary.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/grpc-binary.plist new file mode 100644 index 00000000..58aee5cc --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/grpc-binary.plist @@ -0,0 +1,218 @@ + + + + + PreferenceSpecifiers + + + FooterText + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/gtm-session-fetcher.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/gtm-session-fetcher.plist new file mode 100644 index 00000000..9a823443 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/gtm-session-fetcher.plist @@ -0,0 +1,219 @@ + + + + + PreferenceSpecifiers + + + FooterText + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/interop-ios-for-google-sdks.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/interop-ios-for-google-sdks.plist new file mode 100644 index 00000000..9a823443 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/interop-ios-for-google-sdks.plist @@ -0,0 +1,219 @@ + + + + + PreferenceSpecifiers + + + FooterText + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/kakao-ios-sdk.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/kakao-ios-sdk.plist new file mode 100644 index 00000000..58aee5cc --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/kakao-ios-sdk.plist @@ -0,0 +1,218 @@ + + + + + PreferenceSpecifiers + + + FooterText + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/leveldb.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/leveldb.plist new file mode 100644 index 00000000..94888ca5 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/leveldb.plist @@ -0,0 +1,44 @@ + + + + + PreferenceSpecifiers + + + FooterText + Copyright (c) 2011 The LevelDB Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + License + BSD-3-Clause + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/nanopb.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/nanopb.plist new file mode 100644 index 00000000..68167076 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/nanopb.plist @@ -0,0 +1,37 @@ + + + + + PreferenceSpecifiers + + + FooterText + Copyright (c) 2011 Petteri Aimonen <jpa at nanopb.mail.kapsi.fi> + +This software is provided 'as-is', without any express or +implied warranty. In no event will the authors be held liable +for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you use + this software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and + must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + + License + Zlib + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/promises.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/promises.plist new file mode 100644 index 00000000..9a823443 --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/promises.plist @@ -0,0 +1,219 @@ + + + + + PreferenceSpecifiers + + + FooterText + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/swift-protobuf.plist b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/swift-protobuf.plist new file mode 100644 index 00000000..f030b65d --- /dev/null +++ b/MLS/MLS/Resource/Settings.bundle/com.mono0926.LicensePlist/swift-protobuf.plist @@ -0,0 +1,228 @@ + + + + + PreferenceSpecifiers + + + FooterText + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. + + License + Apache-2.0 + Type + PSGroupSpecifier + + + + diff --git a/MLS/MLS/Resource/Settings.bundle/en.lproj/Root.strings b/MLS/MLS/Resource/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 0000000000000000000000000000000000000000..8cd87b9d6b20c1fbf87bd4db3db267fca5ad4df9 GIT binary patch literal 546 zcmaixOHRW;5JYRuDMndFh#Ua1V1d}N;sVAV2TO?uC3a9aJn*VxFrY}tnon0(S66#J z-d9>G>6W!ur(SDqlp`9nn~*(m%iWnv?yq`Qfp6XbK1?+om~~#r)ZnhkYQU_VbfjuT zHNn`CX<0sd*m1A}>&5sU$akD=GTXJ1e literal 0 HcmV?d00001 diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift index ea446b86..1f3c19ef 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift @@ -119,6 +119,11 @@ extension CustomerSupportBaseViewController { case 1: guard let viewController = policyFactory?.make(type: .service) else { return } navigationController?.pushViewController(viewController, animated: true) + case 2: + guard let url = URL(string: UIApplication.openSettingsURLString) else { return } + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } default: break } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/TermsViewController.swift similarity index 100% rename from MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsViewController.swift rename to MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/TermsViewController.swift diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsDetailViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsDetailViewController.swift deleted file mode 100644 index cd26c893..00000000 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsDetailViewController.swift +++ /dev/null @@ -1,9 +0,0 @@ -import UIKit - -final class TermsDetailViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .blue - } -} diff --git a/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyType.swift b/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyType.swift index a2f39c25..aa334654 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyType.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyType.swift @@ -3,6 +3,7 @@ import Foundation public enum PolicyType: CaseIterable { case service case privacy + case openSource public var title: String { switch self { @@ -10,15 +11,19 @@ public enum PolicyType: CaseIterable { "서비스 이용약관" case .privacy: "개인정보 처리방침" + case .openSource: + "오픈소스 라이선스" } } public var fileName: String { switch self { case .service: - return "TermsOfService.txt" + "TermsOfService.txt" case .privacy: - return "PrivacyPolicy.txt" + "PrivacyPolicy.txt" + case .openSource: + "" } } From a0e69a28030ef7dfdebb566cfca276faab47186a Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 11 Dec 2025 21:58:51 +0900 Subject: [PATCH 27/34] =?UTF-8?q?fix/#273:=20=EB=8F=84=EA=B0=90=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=98=A8=EB=B3=B4=EB=94=A9=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 --- .../guideArrow1.imageset/guideArrow1.png | Bin 1394 -> 1562 bytes .../guideArrow2.imageset/guideArrow2.png | Bin 415 -> 1890 bytes .../DictionaryDetailBaseViewController.swift | 11 ++++------- .../OnBoarding/DetailOnBoardingView.swift | 13 ++++++------- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/guideArrow1.png b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/guideArrow1.png index 885fb7a127f358435eba97c8c5c70f4d5f503596..07e674cb4ee9e52afaf2e986beda4eaf10220361 100644 GIT binary patch delta 1529 zcmb7EdpOez7&faZ4Y}nQ&Jf3Rley<{A0l@pa@k5dZe@~dF2AYVa+XW0Lx+r-)5Tm0 zom|>Hi4fAl+2(GEY|RKim*&j*^Za+-=lQ<(d7tllzxTgyN%}rqE7!}zSwNx z<8l(?BWA`(8)D7)hKC+v^MCE(&U z<8V~TL(1Vk5VAB`mLf}*eIQ}cQ)N|IXRiM4&D8cE?I1Lw5P6`t?QL5zM11(aAnq$_ z;MX{{MwXqK&mssbt|zcv}wL$z)kAH3kx$zKlciw@027ODz7dsTW!i)$o|%B zi0j4#?Aihjev)3kd0{Mm-Nkkgc(bIH57^NF>}}J2OK_du`B=zoA_B{u12WZc1NJ(~>Tzq3PDm z`#Yr@PQKHc+uTbiKkEGfx!y}u*N?mQI+8s~(YlMz!V;A&FK!wfK#Lmt7Al^lhrs`8 zaA*QbEjY-itM5ft=@37ABLE)m4Bp`7=Xr%=$NLKS*v~1bVMI){v|P21T;zl)Yplm6 z?OGV))m&F_g>QsXm0gfVT7TKhHebd(TUCB|Cf^6#S zxbWpSnc`E^3__eXhrayk!DW@e!XJiGnMX^v0a0l z5)}Mq_J>u>Iag*E!C1?vcVcxEQEGW9QVoS4S0WRh5Xsht1p z@vsA*lggr-YToK1bQcEDZ3fe#!nCoRJ7wdOSNA*)BaKM0Xk{hK zEyFPQyyWM+>5UE2v4Zn&Q<0A-U`90XB8aEsG`QMv`j$=f6}xI-`$X-VzUbLU)**l`N73dZ*dFbtJ20SB6<+$>WRlb=mk@~eTWWPa z-+O2EPeIz?Hzl;9{1)jzlRxHskgsZt+sxX`6B+*#nWkStW_WP_W{1W6l5SQcWX1Y8 z3P6L0q4$nygXMeEm9(@$WJi-G%Fb<3&FfZSJ66q0niLg33?tjm+%x!>s?0vY$k!*k zBK6%OegW&PrSk&zq*v#69u4KT29h17?0z=`Sx_l_A+VMu%J_+?<|VssX-zBsJF5s) zov15zAk60QhWmQZ+Te?^rd>deHZUg6QCOEvnKOZrF`{NV;EA_&;R@Eyise+$v3xeM zNZl1VP}QiuN?gimMn;)berF-PisY%Dmu(}soWYDc9$r7MBa_bT_;(*`gis3!HF8wB zo`W7{UH5aDef54Dp#K1YD#t=P77->}j3~xSjaI5)r5n)C8(@c!yGnlSsD2(+)eG_5 z#*f6yAJX0o9pYNH)uYABRx7%j58amf)}O#pq*Np8ZV}2?Wwdby_85*T%nB^$E#!um z9|;YPnTXpy4JB+ci3$-(i@m>jl3mW%Zx*nL3f*hmmuL#+$7F&gsJbPNS_%teU|sc)^0nj-#8uH z3d#DqO_w>T)1`(Bw<*_yDzN;2-H-$(kJ1x#KhR-11v3VWUgui*E!4R+XiP5?TYXMt zZcVUjyA6_yH-jZzom-*1=gtLsmV54evvqm<)>@vx}n#rp6w3{U2_g B*fjtE delta 1360 zcmV-W1+V&=4Dt#giBL{Q4GJ0x0000DNk~Le0001Z0001g2nGNE03=}sYLOu|e+NlK zK~#7F?cIxU+c*#g;AQ3t@O8lJARQb#ICl_rkaS>n5MKrIRp7Y_=v0t&0C(VY@CO)B ziJg(e1^5sa`o9^@Boo+o`u$jIIexfyp^OB4&xAs8N8A;I!p=vW%jNFabEdug=~F+ZqmaEEe~-*;xm>F z?BOQ14*KQAozQad_O8OEe+-DzlsB(_&Yj}x_Cm{8A+Q7DmfM>nKTCW6Q!6yj!hsDX z_#P8GClSl^lzX9k3kP<9n9Y+{%T3!=h=(KMo;mWxd#h0d%WXgIqJj z1@Y99e9^2O-#6!*ryy^=b|~RP1BXSYA-1=VNOO@M@+p|EI4oe-+Vo>DWCw#=F~lW5 z2De6ti}eg{e~pl~&4%YaS%Nr`p$+b%kS);fehd>HoPM+f`W~|b7Y=NJ?g%~x*CM2K zs<>EhO+s+F&Bhw&+udHs7U*tf4zBx_NQBCy@9S3+s-gltJD&ApI4(Cvui0bUkCKcUeIf7t^40)8)K4|H?QEX@{yr?b@! zJe{pxIDhQV&(h57H3~fTwqD?&)pb*5FZWhiq|9D+tgHf`e}%Z2Zb~m?6Ld3It&mMP|ERTqohz%9<#{J$&Y>5rd7RplkU56{|2^@#r>1G1 zh0HkwxSMz#$G%oUcg$}qWKziBZsIlHSf1?)0iJ4qzX+KX0z9>!`ANvECN!`UulduL z=mH^o!_eqS$h;pX4dtrJ Sx0>Pr0000_c6r#0JNm?_xtRhMM(v{hyBoi_;uCrtJ_s?(7?{m)cywCHz&iOpg`JCtT`DA%} zZeN05jt2lNad&gxrB=zJ;WX5K*X!@GYSD~z+ZzMGI*&zz#LP1l>Le7iYr7-3)@Afj z9iYS5JJ|sInnRci#sHx4)!mux8xP6f$fI7l65GbPX8opq#4`9Q%9P}9>|8;O!x40{zF`eIu zhRAFB=3Imgr=y73OjCer1s5M&p0!?`YI&*{<5bwL7`AkMF*mCi&$JYxL#Q+fYuv1= zf8OiB{vzyvt-~`*y$C4*%@>i8U#=I^7*7uf8*|w4#H}WaTcO7XLbN1_(q%A%RXaAx z75?NxQl4awF^-nq&d#B~h%MZ2*Ay1?rrgnX0EL6dw8_iNpL4v%3zZ!cD{V>QTtS+T zW>Gh0IbWvb&9t7lGk8xoEy_^!z8KCfDT46Lji;eu{!>EKc+~z(pJ`Rqd2Ym3Ck&aN zZYWA%%)EM@)>D1lC&U;GeAkzB=e6Bv!+8SKC}>#0{umu@@{=!K`>i>fc959xYUKVj zwILaeWrLo(dtP3BKV3SsJLg9+*@mhyMH{ka4c10@+%-@f?2KH8U^6H)@te2H4Ii`7IGS18i%+T}V zCa_%>S}m*bHS3XShE`zW!EnL_i>1p$(<9@9%M3-vO<#M6sk;y-e1=zwdn$`$Pi_PA z*;c5!6TeFSARZ*y>|=0XYl%E96;7G4U*cf zeh_uM-LLzK^NW=>#9G!woVczaHZ8ZBPthtS{M!c#t=i<~5x@`G38t;IGd$Q!X_8Ju zzYGs3nxxp6vIUBEu?ycEn_?uQq_>t*(p{|M((sGS#d1nxvV|-$kPqqUx8`mLu{(B` z&Gd$Z6}Ma%6q{AB>{!$x@3jn5un-L;%3Hn1(O45`f33Iy?R&w3H+RM&j zERCVa$`=N{YYXyIJ<*a|5#oKbyxvw*@EeztXfossgaLGUC)y+^I3|8Q{T|92GP2fA zC$Q(c zG}a25xbOy|kEZJ6_jK8dFi?A%Es28(XM-=I$x(vT^DpB0cY+J_n|VZJYd41DtmYVG zy*B~Q&EyEofdvlepQt9D>%Qq^hf{#T~EZ5yL# zE!rbG_tA+dJ>F1FIGTzQVp*HvT&xAi=bNjQ##;6Ne5RwIw@Z89I&~9{f-q9Oy8Y%~ zIEkyDXMuMqNvtd!YXx3>cj-ur#@Y|(4-&ub>#fnfW=xW7lYh#oJ9H}029~XyS3x)= znTM$-)vW>x>Z?y*Jhh;bI%^G9(XgRN6jB<9r6GrSDwRnWVukz60>Th{EESf$qKM~t z-C02Ciji3TCn!Fc3=C2|eL+**VHHgqp|j0lS#N;2kih9v{e9e)c`X@M@7m)w} delta 395 zcmV;60d)T24xa-yiBL{Q4GJ0x0000DNk~Le0000D0000e2nGNE040iW`2YX_32;bR za{vGf6951U69E94oEVWdAAbQfNklF8+ zocK(QQ5Y7rNRDezrwzsu!(x;{k00rDGN<;r#q! zM)%LWf`m>s*J2l1oU=#RLts%O+A%Ld3wnQZU#pl$!hLXM3(uQB9foQ8MN>gjMWO{52BNt#-3w7fEM1S)vd9t p6^4f!7BwlNmM3RZM8(twx&s}3c<~A}C#nDd002ovPDHLkV1gs2qq6`2 diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index f0c9c994..eb772707 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -79,10 +79,6 @@ class DictionaryDetailBaseViewController: BaseViewController { configureUI() bind() // 액션 바인딩 setupMenu(type.detailTypes) - let vc = detailOnBoardingFactory.make() - vc.modalPresentationStyle = .overFullScreen - vc.modalTransitionStyle = .crossDissolve - present(vc, animated: true) } override func viewDidLayoutSubviews() { @@ -111,6 +107,7 @@ private extension DictionaryDetailBaseViewController { func configureUI() { mainView.scrollView.delegate = self + checkVisited() } } @@ -298,7 +295,7 @@ extension DictionaryDetailBaseViewController { ) return } - + let itemId = id(item) if isBookmarked(item) { @@ -347,7 +344,7 @@ extension DictionaryDetailBaseViewController { } } } - + func checkVisited() { fetchVisitBookmarkUseCase.execute() .withUnretained(self) @@ -400,7 +397,7 @@ extension DictionaryDetailBaseViewController { ctaText: "건의하기", cancelText: "취소", ctaAction: { [weak self] in - guard let self = self else { return } + guard self != nil else { return } if let url = URL(string: urlString) { UIApplication.shared.open(url) } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingView.swift index cbc78122..798bcbc8 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingView.swift @@ -12,10 +12,9 @@ class DetailOnBoardingView: UIView { static let trailingMargin: CGFloat = 8 static let iconSize: CGFloat = 36 static let contentViewSize: CGFloat = 44 - static let arrowSize: CGFloat = 28 + static let arrowSize: CGFloat = 48 static let arrowTrailing: CGFloat = 28 - static let firstArrowMargin: CGFloat = 10 - static let secondArrowMargin: CGFloat = 15 + static let arrowMargin: CGFloat = 6 static let alertHeight: CGFloat = 220 static let alertWidth: CGFloat = 328 static let buttonWitdh: CGFloat = 96 @@ -146,7 +145,7 @@ private extension DetailOnBoardingView { func setupConstraints() { iconContentView.snp.makeConstraints { make in make.top.equalToSuperview() - make.leading.equalTo(firstArrow.snp.trailing).offset(Constant.firstArrowMargin) + make.leading.equalTo(firstArrow.snp.trailing) make.trailing.equalToSuperview().inset(Constant.trailingMargin) make.size.equalTo(Constant.contentViewSize) } @@ -157,7 +156,7 @@ private extension DetailOnBoardingView { } firstLabel.snp.makeConstraints { make in - make.top.equalTo(firstArrow.snp.bottom).offset(Constant.firstArrowMargin) + make.top.equalTo(firstArrow.snp.bottom) make.trailing.equalToSuperview().inset(Constant.trailingMargin) } @@ -168,13 +167,13 @@ private extension DetailOnBoardingView { } secondArrow.snp.makeConstraints { make in - make.top.equalTo(alertView.snp.bottom).offset(Constant.secondArrowMargin) + make.top.equalTo(alertView.snp.bottom).offset(Constant.arrowMargin) make.centerX.equalTo(firstLabel) make.size.equalTo(Constant.arrowSize) } secondLabel.snp.makeConstraints { make in - make.top.equalTo(secondArrow.snp.bottom).offset(Constant.secondArrowMargin) + make.top.equalTo(secondArrow.snp.bottom).offset(Constant.arrowMargin) make.trailing.equalTo(secondArrow.snp.trailing).offset(Constant.arrowTrailing) } From 055eaf35d89c527968807c29bb8a10aa7e53e810 Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 11 Dec 2025 22:07:36 +0900 Subject: [PATCH 28/34] =?UTF-8?q?fix/#273:=20DetailStackInfoView.reset()?= =?UTF-8?q?=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ItemDictionaryDetailViewController.swift | 1 + .../QuestDictionaryDetailViewController.swift | 1 + .../SectionStackView/DetailStackInfoView.swift | 17 +++++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift index b1d727c8..27b25de1 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift @@ -36,6 +36,7 @@ private extension ItemDictionaryDetailViewController { // descriptionText detailInfoView.descriptionLabel.text = infos.descriptionText ?? "" + detailInfoView.reset() if let npcPrice = infos.npcPrice { detailInfoView.addInfo(mainText: "상점판매가", subText: "\(npcPrice.formatted()) 메소") } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift index c53bb310..3eea7f99 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift @@ -35,6 +35,7 @@ private extension QuestDictionaryDetailViewController { contentViews.append(detailInfoView) // 뭘로 빈페이지 보여줄지 정하지.. + detailInfoView.reset() if !(detailInfos.startNpcName == nil) { contentViews.append(detailInfoView) // 완료조건 추가 diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift index 601bffcb..ccbe74fd 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift @@ -211,6 +211,23 @@ extension DetailStackInfoView { func addInfo(mainText: String, subText: String) { addInfoRow(to: infoStackView, mainText: mainText, subText: subText) } + + /// 현재 표시 중인 모든 스택뷰 내용을 초기화 + func reset() { + infoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + detailInfoStackView.arrangedSubviews + .filter { $0 !== detailInfoTitleLabelView } + .forEach { $0.removeFromSuperview() } + + completeConditionStackView.arrangedSubviews + .filter { $0 !== completeConditionTitleLabelView } + .forEach { $0.removeFromSuperview() } + + rewardStackView.arrangedSubviews + .filter { $0 !== rewardTitleLabelView } + .forEach { $0.removeFromSuperview() } + } private func addInfoRow( to stackView: UIStackView, From 3c9846be1ec4bb9eeb3dae8cf064d02d0a3933ba Mon Sep 17 00:00:00 2001 From: p2glet Date: Thu, 11 Dec 2025 22:11:50 +0900 Subject: [PATCH 29/34] =?UTF-8?q?fix/#273:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20radius=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 --- .../MyPageFeature/MyPageFeature/Main/MyPageMainCell.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainCell.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainCell.swift index 7b8915cc..a02d2651 100644 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainCell.swift +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/Main/MyPageMainCell.swift @@ -16,6 +16,7 @@ public final class MyPageMainCell: UICollectionViewCell { static let buttonHeight: CGFloat = 44 static let horizontalInset: CGFloat = 16 static let verticalInset: CGFloat = 20 + static let radius: CGFloat = 42 } // MARK: - Properties @@ -27,7 +28,7 @@ public final class MyPageMainCell: UICollectionViewCell { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true - imageView.layer.cornerRadius = Constant.imageSize / 2 + imageView.layer.cornerRadius = Constant.radius return imageView }() From 0d255689c844fffd520d0e9e8354ff68e912da14 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Dec 2025 13:12:51 +0000 Subject: [PATCH 30/34] style/#273: Apply SwiftLint autocorrect --- MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift | 4 ++-- .../DomainInterface/Repository/UserDefaultsRepository.swift | 4 ++-- MLS/MLS/Application/AppDelegate.swift | 5 ++--- .../DesignSystem/DesignSystem/Components/CommonButton.swift | 3 +-- .../DictionaryDetailBaseViewController.swift | 2 +- .../SectionStackView/DetailStackInfoView.swift | 2 +- .../DictionaryNotificationViewController.swift | 2 +- .../DictionaryFeature/DictionarySearch/EmptyRecentCell.swift | 2 +- 8 files changed, 11 insertions(+), 13 deletions(-) diff --git a/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift index 30d8a9cf..e8ea829f 100644 --- a/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift @@ -69,7 +69,7 @@ public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { return Disposables.create() } } - + public func fetchBookmark() -> Observable { return Observable.create { observer in let hasVisited = UserDefaults.standard.bool(forKey: self.bookmarkkey) @@ -86,7 +86,7 @@ public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { return Disposables.create() } } - + public func fetchDictionaryDetail() -> Observable { return Observable.create { observer in let hasVisited = UserDefaults.standard.bool(forKey: self.dictionaryDetailkey) diff --git a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift index 8662e4fe..763c10d2 100644 --- a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift +++ b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift @@ -7,10 +7,10 @@ public protocol UserDefaultsRepository { func fetchPlatform() -> Observable func savePlatform(platform: LoginPlatform) -> Completable - + func fetchBookmark() -> Observable func saveBookmark() -> Completable - + func fetchDictionaryDetail() -> Observable func saveDictionaryDetail() -> Completable } diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index dbd77f20..53d2cff0 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -18,9 +18,9 @@ import Firebase import KakaoSDKCommon import MyPageFeature import MyPageFeatureInterface +import os import UIKit import UserNotifications -import os @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -397,8 +397,7 @@ extension AppDelegate { ) ) } - DIContainer.register(type: FetchDictionaryDetailMonsterMapUseCase.self) - { + DIContainer.register(type: FetchDictionaryDetailMonsterMapUseCase.self) { FetchDictionaryDetailMonsterMapUseCaseImpl( repository: DIContainer.resolve( type: DictionaryDetailAPIRepository.self diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift b/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift index ca89df0b..738acf56 100644 --- a/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift +++ b/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift @@ -125,8 +125,7 @@ private extension CommonButton { config.background.backgroundColor = .clear let currentTitle = isEnabled ? title : disabledTitle if let textButtonTitle = currentTitle, - let lineHeight = style.font?.lineHeight - { + let lineHeight = style.font?.lineHeight { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.minimumLineHeight = lineHeight * Constant.textLineHeight paragraphStyle.maximumLineHeight = lineHeight * Constant.textLineHeight diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index eb772707..1d5b39f5 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -348,7 +348,7 @@ extension DictionaryDetailBaseViewController { func checkVisited() { fetchVisitBookmarkUseCase.execute() .withUnretained(self) - .subscribe{ owner, isVisit in + .subscribe { owner, isVisit in if !isVisit { let viewController = owner.detailOnBoardingFactory.make() viewController.modalPresentationStyle = .overFullScreen diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift index ccbe74fd..00670a7f 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift @@ -211,7 +211,7 @@ extension DetailStackInfoView { func addInfo(mainText: String, subText: String) { addInfoRow(to: infoStackView, mainText: mainText, subText: subText) } - + /// 현재 표시 중인 모든 스택뷰 내용을 초기화 func reset() { infoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift index 679768bd..0a85b474 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift @@ -14,7 +14,7 @@ public final class DictionaryNotificationViewController: BaseViewController, Vie // MARK: - Properties public var disposeBag = DisposeBag() - + private var lastPagingTime: Date = .distantPast private var notificationSettingFactory: NotificationSettingFactory diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/EmptyRecentCell.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/EmptyRecentCell.swift index 2a2f418d..cfc2ab71 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/EmptyRecentCell.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionarySearch/EmptyRecentCell.swift @@ -24,7 +24,7 @@ private extension EmptyRecentCell { func addViews() { contentView.addSubview(label) } - + func setUpConstraints() { label.snp.makeConstraints { $0.edges.equalToSuperview() From 7981c6cf286d59f22bb3f51d48a8a9ba2d65ea77 Mon Sep 17 00:00:00 2001 From: p2glet Date: Fri, 12 Dec 2025 00:05:10 +0900 Subject: [PATCH 31/34] =?UTF-8?q?fix/#273:=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Endpoints/DictionaryDetailEndPoint.swift | 16 +++++++-------- .../DictionaryDetailAPIRepositoryImpl.swift | 20 +++++++++++-------- ...ctionaryDetailItemMonsterUseCaseImpl.swift | 2 +- ...naryDetailMapSpawnMonsterUseCaseImpl.swift | 2 +- ...naryDetailMonsterDropItemUseCaseImpl.swift | 2 +- ...hDictionaryDetailNpcQuestUseCaseImpl.swift | 2 +- .../Entity/Filter/SortType.swift | 10 +++++++--- .../DictionaryDetailAPIRepository.swift | 8 ++++---- ...ctionaryDetailItemDropMonsterUseCase.swift | 2 +- ...ctionaryDetailMapSpawnMonsterUseCase.swift | 2 +- ...hDictionaryDetailMonsterItemsUseCase.swift | 2 +- ...FetchDictionaryDetailNpcQuestUseCase.swift | 2 +- .../Item/ItemDictionaryDetailReactor.swift | 12 ++--------- .../Map/MapDictionaryDetailReactor.swift | 2 +- .../MonsterDictionaryDetailReactor.swift | 11 +--------- .../NPC/NpcDictionaryDetailReactor.swift | 9 +-------- 16 files changed, 44 insertions(+), 60 deletions(-) diff --git a/MLS/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift b/MLS/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift index 5484e6a7..cbd7adb8 100644 --- a/MLS/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift +++ b/MLS/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift @@ -12,8 +12,8 @@ public enum DictionaryDetailEndPoint { } // 몬스터 디테일 드롭아이템 - public static func fetchMonsterDetailDropItem(id: Int, sort: [String]?) -> ResponsableEndPoint<[DictionaryDetailMonsterDropItemResponseDTO]> { - return .init(baseURL: base, path: "/api/v1/monsters/\(id)/items", method: .GET, query: ["sort": sort?.joined(separator: ",")]) + public static func fetchMonsterDetailDropItem(id: Int, query: Encodable) -> ResponsableEndPoint<[DictionaryDetailMonsterDropItemResponseDTO]> { + return .init(baseURL: base, path: "/api/v1/monsters/\(id)/items", method: .GET, query: query) } // 몬스터 디테일 출현맵 @@ -26,8 +26,8 @@ public enum DictionaryDetailEndPoint { return .init(baseURL: base, path: "/api/v1/npcs/\(id)", method: .GET) } // Npc 디테일 퀘스트 - public static func fetchNpcDetailQuest(id: Int, sort: [String]?) -> ResponsableEndPoint<[DictionaryDetailNpcQuestResponseDTO]> { - return .init(baseURL: base, path: "/api/v1/npcs/\(id)/quests", method: .GET, query: ["sort": sort?.joined(separator: ",")]) + public static func fetchNpcDetailQuest(id: Int, query: Encodable) -> ResponsableEndPoint<[DictionaryDetailNpcQuestResponseDTO]> { + return .init(baseURL: base, path: "/api/v1/npcs/\(id)/quests", method: .GET, query: query) } // NPC 디테일 맵 public static func fetchNpcDetailMap(id: Int) -> ResponsableEndPoint<[DictionaryDetailMonsterMapResponseDTO]> { @@ -38,8 +38,8 @@ public enum DictionaryDetailEndPoint { return .init(baseURL: base, path: "/api/v1/items/\(id)", method: .GET) } // Item 디테일 드롭몬스터 상세정보 - public static func fetchItemDetailDropMonster(id: Int, sort: [String]?) -> ResponsableEndPoint<[DictionaryDetailItemDropMonsterResponseDTO]> { - return .init(baseURL: base, path: "/api/v1/items/\(id)/monsters", method: .GET, query: ["sort": sort?.joined(separator: ",")]) + public static func fetchItemDetailDropMonster(id: Int, query: Encodable) -> ResponsableEndPoint<[DictionaryDetailItemDropMonsterResponseDTO]> { + return .init(baseURL: base, path: "/api/v1/items/\(id)/monsters", method: .GET, query: query) } // Quest 디테일 상세정보 @@ -58,8 +58,8 @@ public enum DictionaryDetailEndPoint { } // Map 디테일 출현 몬스터 - public static func fetchMapDetailSpawnMonster(id: Int, sort: [String]?) -> ResponsableEndPoint<[DictionaryDetailMapSpawnMonsterResponseDTO]> { - return .init(baseURL: base, path: "/api/v1/maps/\(id)/monsters", method: .GET, query: ["sort": sort?.joined(separator: ",")]) + public static func fetchMapDetailSpawnMonster(id: Int, query: Encodable) -> ResponsableEndPoint<[DictionaryDetailMapSpawnMonsterResponseDTO]> { + return .init(baseURL: base, path: "/api/v1/maps/\(id)/monsters", method: .GET, query: query) } // Map 디테일 출현 npc diff --git a/MLS/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift b/MLS/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift index dfcf6bde..83f4faf0 100644 --- a/MLS/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift @@ -18,8 +18,8 @@ public final class DictionaryDetailAPIRepositoryImpl: DictionaryDetailAPIReposit return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } } - public func fetchMonsterDetailDropItem(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMonsterDropItemResponse]> { - let endPoint = DictionaryDetailEndPoint.fetchMonsterDetailDropItem(id: id, sort: sort) + public func fetchMonsterDetailDropItem(id: Int, sort: String?) -> Observable<[DictionaryDetailMonsterDropItemResponse]> { + let endPoint = DictionaryDetailEndPoint.fetchMonsterDetailDropItem(id: id, query: SortQuery(sort: sort)) return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map {$0.map {$0.toDomain()}} } @@ -33,8 +33,8 @@ public final class DictionaryDetailAPIRepositoryImpl: DictionaryDetailAPIReposit return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } } - public func fetchNpcDetailQuest(id: Int, sort: [String]?) -> Observable<[DictionaryDetailNpcQuestResponse]> { - let endPoint = DictionaryDetailEndPoint.fetchNpcDetailQuest(id: id, sort: sort) + public func fetchNpcDetailQuest(id: Int, sort: String?) -> Observable<[DictionaryDetailNpcQuestResponse]> { + let endPoint = DictionaryDetailEndPoint.fetchNpcDetailQuest(id: id, query: SortQuery(sort: sort)) return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.map {$0.toDomain()} } } @@ -49,8 +49,8 @@ public final class DictionaryDetailAPIRepositoryImpl: DictionaryDetailAPIReposit return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } } - public func fetchItemDetailDropMonster(id: Int, sort: [String]?) -> Observable<[DictionaryDetailItemDropMonsterResponse]> { - let endPoint = DictionaryDetailEndPoint.fetchItemDetailDropMonster(id: id, sort: sort) + public func fetchItemDetailDropMonster(id: Int, sort: String?) -> Observable<[DictionaryDetailItemDropMonsterResponse]> { + let endPoint = DictionaryDetailEndPoint.fetchItemDetailDropMonster(id: id, query: SortQuery(sort: sort)) return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.map {$0.toDomain() } } } @@ -69,8 +69,8 @@ public final class DictionaryDetailAPIRepositoryImpl: DictionaryDetailAPIReposit return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.toDomain() } } - public func fetchMapDetailSpawnMonster(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> { - let endPoint = DictionaryDetailEndPoint.fetchMapDetailSpawnMonster(id: id, sort: sort) + public func fetchMapDetailSpawnMonster(id: Int, sort: String?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> { + let endPoint = DictionaryDetailEndPoint.fetchMapDetailSpawnMonster(id: id, query: SortQuery(sort: sort)) return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.map {$0.toDomain()} } } @@ -79,3 +79,7 @@ public final class DictionaryDetailAPIRepositoryImpl: DictionaryDetailAPIReposit return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map {$0.map {$0.toDomain()}} } } + +struct SortQuery: Encodable { + let sort: String? +} diff --git a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailItemMonsterUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailItemMonsterUseCaseImpl.swift index 133f0224..322f67d1 100644 --- a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailItemMonsterUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailItemMonsterUseCaseImpl.swift @@ -9,7 +9,7 @@ public final class FetchDictionaryDetailItemDropMonsterUseCaseImpl: FetchDiction self.repository = repository } - public func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailItemDropMonsterResponse]> { + public func execute(id: Int, sort: String?) -> Observable<[DictionaryDetailItemDropMonsterResponse]> { return repository.fetchItemDetailDropMonster(id: id, sort: sort) } } diff --git a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCaseImpl.swift index eeea136d..97ed90a1 100644 --- a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCaseImpl.swift @@ -7,7 +7,7 @@ public final class FetchDictionaryDetailMapSpawnMonsterUseCaseImpl: FetchDiction self.repository = repository } - public func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> { + public func execute(id: Int, sort: String?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> { return repository.fetchMapDetailSpawnMonster(id: id, sort: sort) } } diff --git a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMonsterDropItemUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMonsterDropItemUseCaseImpl.swift index 91a307d1..c55067ac 100644 --- a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMonsterDropItemUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailMonsterDropItemUseCaseImpl.swift @@ -7,7 +7,7 @@ public final class FetchDictionaryDetailMonsterDropItemUseCaseImpl: FetchDiction self.repository = repository } - public func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMonsterDropItemResponse]> { + public func execute(id: Int, sort: String?) -> Observable<[DictionaryDetailMonsterDropItemResponse]> { return repository.fetchMonsterDetailDropItem(id: id, sort: sort) } diff --git a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailNpcQuestUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailNpcQuestUseCaseImpl.swift index aa64d319..5eee5646 100644 --- a/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailNpcQuestUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/DictionaryDetail/FetchDictionaryDetailNpcQuestUseCaseImpl.swift @@ -7,7 +7,7 @@ public final class FetchDictionaryDetailNpcQuestUseCaseImpl: FetchDictionaryDeta self.repository = repository } - public func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailNpcQuestResponse]> { + public func execute(id: Int, sort: String?) -> Observable<[DictionaryDetailNpcQuestResponse]> { return repository.fetchNpcDetailQuest(id: id, sort: sort) } } diff --git a/MLS/Domain/DomainInterface/Entity/Filter/SortType.swift b/MLS/Domain/DomainInterface/Entity/Filter/SortType.swift index a16d08eb..56949996 100644 --- a/MLS/Domain/DomainInterface/Entity/Filter/SortType.swift +++ b/MLS/Domain/DomainInterface/Entity/Filter/SortType.swift @@ -22,6 +22,10 @@ public enum SortType: String { return "level" case .expASC, .expDESC: return "exp" + case .mostAppear: + return "maxSpawnCount" + case .mostDrop: + return "dropRate" default: return "" } @@ -30,9 +34,9 @@ public enum SortType: String { public var direction: String { switch self { case .expASC, .levelASC, .korean: - return "ASC" - case .expDESC, .levelDESC: - return "DESC" + return "asc" + case .expDESC, .levelDESC, .mostDrop, .mostAppear: + return "desc" default: return "" } diff --git a/MLS/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift b/MLS/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift index d080c997..4f593334 100644 --- a/MLS/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift +++ b/MLS/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift @@ -4,7 +4,7 @@ public protocol DictionaryDetailAPIRepository { // 몬스터 디테일 상세정보 func fetchMonsterDetail(id: Int) -> Observable // 몬스터 디테일 드롭 아이템 - func fetchMonsterDetailDropItem(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMonsterDropItemResponse]> + func fetchMonsterDetailDropItem(id: Int, sort: String?) -> Observable<[DictionaryDetailMonsterDropItemResponse]> // 몬스터 디테일 출현맵 func fetchMonsterDetailMap(id: Int) -> Observable<[DictionaryDetailMonsterMapResponse]> @@ -12,13 +12,13 @@ public protocol DictionaryDetailAPIRepository { func fetchNpcDetail(id: Int) -> Observable // NPC 디테일 퀘스트 - func fetchNpcDetailQuest(id: Int, sort: [String]?) -> Observable<[DictionaryDetailNpcQuestResponse]> + func fetchNpcDetailQuest(id: Int, sort: String?) -> Observable<[DictionaryDetailNpcQuestResponse]> // NPC 디테일 맵 func fetchNpcDetailMap(id: Int) -> Observable<[DictionaryDetailMonsterMapResponse]> // Item 디테일 상세정보 func fetchItemDetail(id: Int) -> Observable // Item 디테일 드롭 몬스터 - func fetchItemDetailDropMonster(id: Int, sort: [String]?) -> Observable<[DictionaryDetailItemDropMonsterResponse]> + func fetchItemDetailDropMonster(id: Int, sort: String?) -> Observable<[DictionaryDetailItemDropMonsterResponse]> // Quest 디테일 상세정보 func fetchQuestDetail(id: Int) -> Observable // Quest 연계 퀘스트 상세정보 @@ -26,7 +26,7 @@ public protocol DictionaryDetailAPIRepository { // Map 디테일 상세정보 func fetchMapDetail(id: Int) -> Observable // Map 디테일 출현 몬스터 정보 - func fetchMapDetailSpawnMonster(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> + func fetchMapDetailSpawnMonster(id: Int, sort: String?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> // Map 디테일 출현 Npc 정보 func fetchMapDetailNpc(id: Int) -> Observable<[DictionaryDetailMapNpcResponse]> } diff --git a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailItemDropMonsterUseCase.swift b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailItemDropMonsterUseCase.swift index c78a8679..4846a970 100644 --- a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailItemDropMonsterUseCase.swift +++ b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailItemDropMonsterUseCase.swift @@ -1,5 +1,5 @@ import RxSwift public protocol FetchDictionaryDetailItemDropMonsterUseCase { - func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailItemDropMonsterResponse]> + func execute(id: Int, sort: String?) -> Observable<[DictionaryDetailItemDropMonsterResponse]> } diff --git a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCase.swift b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCase.swift index 9921269b..11e6816c 100644 --- a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCase.swift +++ b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMapSpawnMonsterUseCase.swift @@ -1,5 +1,5 @@ import RxSwift public protocol FetchDictionaryDetailMapSpawnMonsterUseCase { - func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> + func execute(id: Int, sort: String?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> } diff --git a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMonsterItemsUseCase.swift b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMonsterItemsUseCase.swift index 93fe6296..85b3e261 100644 --- a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMonsterItemsUseCase.swift +++ b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailMonsterItemsUseCase.swift @@ -1,5 +1,5 @@ import RxSwift public protocol FetchDictionaryDetailMonsterItemsUseCase { - func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailMonsterDropItemResponse]> + func execute(id: Int, sort: String?) -> Observable<[DictionaryDetailMonsterDropItemResponse]> } diff --git a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailNpcQuestUseCase.swift b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailNpcQuestUseCase.swift index 2d9ae126..cad5aa6a 100644 --- a/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailNpcQuestUseCase.swift +++ b/MLS/Domain/DomainInterface/UseCase/DictionaryDetail/FetchDictionaryDetailNpcQuestUseCase.swift @@ -1,5 +1,5 @@ import RxSwift public protocol FetchDictionaryDetailNpcQuestUseCase { - func execute(id: Int, sort: [String]?) -> Observable<[DictionaryDetailNpcQuestResponse]> + func execute(id: Int, sort: String?) -> Observable<[DictionaryDetailNpcQuestResponse]> } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailReactor.swift index 3531c85c..9eece2b5 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailReactor.swift @@ -87,16 +87,8 @@ public final class ItemDictionaryDetailReactor: Reactor { dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: nil).map { .setDetailDropMonsterData($0) } ]) case let .selectFilter(type): - switch type { - case .mostDrop: - return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: ["dropRate", "desc"]).map { .setDetailDropMonsterData($0) } - case .levelDESC: - return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: ["level", "desc"]).map { .setDetailDropMonsterData($0) } - case .levelASC: - return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: ["level", "asc"]).map { .setDetailDropMonsterData($0) } - default: - return .empty() - } + return dictionaryDetailItemDropMonsterUseCase.execute(id: currentState.id, sort: type.sortParameter).map { .setDetailDropMonsterData($0) } + case let .toggleBookmark(isSelected): guard let itemId = currentState.itemDetailInfo.itemId else { return .empty() } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift index 4c2a96f7..cc0a12b1 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift @@ -114,7 +114,7 @@ public final class MapDictionaryDetailReactor: Reactor { ) ) case let .selectFilter(type): - return dictionaryDetailMapSpawnMonsterUseCase.execute(id: currentState.id, sort: ["maxSpawnCount", "asc"]).map { .setDetailSpawnMonsters($0) } + return dictionaryDetailMapSpawnMonsterUseCase.execute(id: currentState.id, sort: type.sortParameter).map { .setDetailSpawnMonsters($0) } case .undoLastDeletedBookmark: guard let lastDeleted = currentState.lastDeletedBookmark, let mapId = lastDeleted.mapId else { return .empty() } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift index f88fd11a..b355a781 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift @@ -106,16 +106,7 @@ public final class MonsterDictionaryDetailReactor: Reactor { ]) case let .selectFilter(type): - switch type { - case .levelDESC: // 레벨 높은 순 - return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: ["level", "desc"]).map { .setDetailDropItemData($0) } - case .levelASC: // 레벨 낮은 순 - return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: ["level", "asc"]).map { .setDetailDropItemData($0) } - case .mostDrop: - return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: nil).map { .setDetailDropItemData($0) } - default: - return .empty() - } + return dictionaryDetailMonsterDropItemUseCase.execute(id: currentState.id, sort: type.sortParameter).map { .setDetailDropItemData($0) } case let .toggleBookmark(isSelected): let monsterId = currentState.monsterDetailInfo.monsterId diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift index 2b409282..d643633f 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift @@ -93,14 +93,7 @@ public final class NpcDictionaryDetailReactor: Reactor { dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: nil).map { .setDetailQuests($0) } ]) case let .selectFilter(type): - switch type { - case .levelHighest: - return dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: ["maxLevel", "desc"]).map { .setDetailQuests($0) } - case .levelLowest: - return dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: ["minLevel", "asc"]).map { .setDetailQuests($0) } - default: - return .empty() - } + return dictionaryDetailNpcQuestUseCase.execute(id: currentState.id, sort: type.sortParameter).map { .setDetailQuests($0) } case let .toggleBookmark(isSelected): let npcId = currentState.npcDetailInfo.npcId From e857c09d2fc814a2e2d1425c12b89931590ca0bd Mon Sep 17 00:00:00 2001 From: p2glet Date: Fri, 12 Dec 2025 01:42:19 +0900 Subject: [PATCH 32/34] =?UTF-8?q?feat/#273:=20authInterceptor=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/AuthAPIRepositoryImpl.swift | 12 ++-- .../Domain/Interceptor/AuthInterceptor.swift | 57 +++++++++++++++++++ .../Domain/Interceptor/TokenInterceptor.swift | 1 + .../AuthAPI/LoginWithKakaoUseCaseImpl.swift | 2 +- .../DomainInterface/Error/AuthError.swift | 1 + MLS/MLS/Application/AppDelegate.swift | 19 ++++--- .../Interface/EmptyViewState.swift | 57 ------------------- 7 files changed, 79 insertions(+), 70 deletions(-) create mode 100644 MLS/Domain/Domain/Interceptor/AuthInterceptor.swift delete mode 100644 MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift diff --git a/MLS/Data/Data/Repository/AuthAPIRepositoryImpl.swift b/MLS/Data/Data/Repository/AuthAPIRepositoryImpl.swift index 6ff36b5b..0cec54a0 100644 --- a/MLS/Data/Data/Repository/AuthAPIRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/AuthAPIRepositoryImpl.swift @@ -7,10 +7,12 @@ import RxSwift public class AuthAPIRepositoryImpl: AuthAPIRepository { private let provider: NetworkProvider private let tokenInterceptor: Interceptor + private let authInterceptor: Interceptor - public init(provider: NetworkProvider, interceptor: Interceptor) { + public init(provider: NetworkProvider, tokenInterceptor: Interceptor, authInterceptor: Interceptor) { self.provider = provider - self.tokenInterceptor = interceptor + self.tokenInterceptor = tokenInterceptor + self.authInterceptor = authInterceptor } public func fetchProfile() -> Observable { @@ -21,7 +23,7 @@ public class AuthAPIRepositoryImpl: AuthAPIRepository { public func loginWithKakao(credential: Credential) -> Observable { let endpoint = AuthEndPoint.loginWithKakao(credential: credential) - return provider.requestData(endPoint: endpoint, interceptor: nil) + return provider.requestData(endPoint: endpoint, interceptor: authInterceptor) .map { $0.toLoginDomain() } .catch { error in if case NetworkError.statusError(let code, _) = error, code == 404 { @@ -34,7 +36,7 @@ public class AuthAPIRepositoryImpl: AuthAPIRepository { public func loginWithApple(credential: Credential) -> Observable { let endpoint = AuthEndPoint.loginWithApple(credential: credential) - return provider.requestData(endPoint: endpoint, interceptor: nil) + return provider.requestData(endPoint: endpoint, interceptor: authInterceptor) .map { $0.toLoginDomain() } .catch { error in if case NetworkError.statusError(let code, _) = error, code == 404 { @@ -76,7 +78,7 @@ public class AuthAPIRepositoryImpl: AuthAPIRepository { public func reissueToken(refreshToken: String) -> Observable { let endPoint = AuthEndPoint.reIssueToken(refreshToken: refreshToken) - return provider.requestData(endPoint: endPoint, interceptor: tokenInterceptor).map { $0.toLoginDomain() } + return provider.requestData(endPoint: endPoint, interceptor: authInterceptor).map { $0.toLoginDomain() } } public func fcmToken(credential: String, fcmToken: String?) -> Completable { diff --git a/MLS/Domain/Domain/Interceptor/AuthInterceptor.swift b/MLS/Domain/Domain/Interceptor/AuthInterceptor.swift new file mode 100644 index 00000000..e9b3c50b --- /dev/null +++ b/MLS/Domain/Domain/Interceptor/AuthInterceptor.swift @@ -0,0 +1,57 @@ +import Foundation + +import DomainInterface + +public final class AuthInterceptor: Interceptor { + private let tokenRepository: TokenRepository + private let authRepository: () -> AuthAPIRepository + + public init(tokenRepository: TokenRepository, authRepository: @escaping () -> AuthAPIRepository) { + self.tokenRepository = tokenRepository + self.authRepository = authRepository + } + + public func adapt(_ request: URLRequest) -> URLRequest { + var request = request + switch tokenRepository.fetchToken(type: .accessToken) { + case .success(let token): + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + case .failure(let error): + print("Failed to fetch access token: \(error)") + } + return request + } + + public func retry(data: Data?, response: URLResponse?, error: Error?) -> Bool { + guard let httpResponse = response as? HTTPURLResponse, + let url = httpResponse.url else { return false } + + // 🚫 reissue 요청은 retry 금지 + if url.path.contains("/auth/reissue") { + print("⚠️ reissue 요청에서는 retry 하지 않음") + return false + } + + if httpResponse.statusCode == 401 { + switch tokenRepository.fetchToken(type: .refreshToken) { + case .success(let refreshToken): + let authRepo = authRepository() + authRepo.reissueToken(refreshToken: refreshToken) + .subscribe(onNext: { newTokens in + _ = self.tokenRepository.saveToken(type: .accessToken, value: newTokens.accessToken) + _ = self.tokenRepository.saveToken(type: .refreshToken, value: newTokens.refreshToken) + print("✅ 토큰 재발급 성공") + }, onError: { error in + print("❌ 토큰 재발급 실패: \(error)") + }) + .dispose() + return true + case .failure(let error): + print("Failed to fetch refresh token: \(error)") + return false + } + } + + return false + } +} diff --git a/MLS/Domain/Domain/Interceptor/TokenInterceptor.swift b/MLS/Domain/Domain/Interceptor/TokenInterceptor.swift index 6cef609a..089a9c95 100644 --- a/MLS/Domain/Domain/Interceptor/TokenInterceptor.swift +++ b/MLS/Domain/Domain/Interceptor/TokenInterceptor.swift @@ -6,6 +6,7 @@ import RxSwift public class TokenInterceptor: Interceptor { private let fetchTokenUseCase: FetchTokenFromLocalUseCase + public init(fetchTokenUseCase: FetchTokenFromLocalUseCase) { self.fetchTokenUseCase = fetchTokenUseCase } diff --git a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithKakaoUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithKakaoUseCaseImpl.swift index 6912e4b6..b19f1f53 100644 --- a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithKakaoUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/LoginWithKakaoUseCaseImpl.swift @@ -21,7 +21,7 @@ public class LoginWithKakaoUseCaseImpl: LoginWithKakaoUseCase { .flatMap { response -> Observable in let saveAccess = self.tokenRepository.saveToken(type: .accessToken, value: response.accessToken) let saveRefresh = self.tokenRepository.saveToken(type: .refreshToken, value: response.refreshToken) - let savePlatform = self.userDefaultsRepository.savePlatform(platform: .apple) + let savePlatform = self.userDefaultsRepository.savePlatform(platform: .kakao) // ✅ 모든 저장 결과 확인 switch (saveAccess, saveRefresh) { diff --git a/MLS/Domain/DomainInterface/Error/AuthError.swift b/MLS/Domain/DomainInterface/Error/AuthError.swift index 20986a27..abc34978 100644 --- a/MLS/Domain/DomainInterface/Error/AuthError.swift +++ b/MLS/Domain/DomainInterface/Error/AuthError.swift @@ -1,4 +1,5 @@ public enum AuthError: Error { case unknown(message: String) case userNotFound(credential: Credential) + case tokenExpired } diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index dbd77f20..9e539423 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -146,20 +146,25 @@ extension AppDelegate { ) { AppleLoginProviderImpl() } - DIContainer.register(type: Interceptor.self) { + DIContainer.register(type: Interceptor.self, name: "tokenInterceptor") { TokenInterceptor( fetchTokenUseCase: DIContainer.resolve( type: FetchTokenFromLocalUseCase.self ) ) } + + DIContainer.register(type: Interceptor.self, name: "authInterceptor") { + AuthInterceptor(tokenRepository: DIContainer.resolve(type: TokenRepository.self), authRepository: { DIContainer.resolve(type: AuthAPIRepository.self) }) + } } fileprivate func registerRepository() { DIContainer.register(type: AuthAPIRepository.self) { AuthAPIRepositoryImpl( provider: DIContainer.resolve(type: NetworkProvider.self), - interceptor: DIContainer.resolve(type: Interceptor.self) + tokenInterceptor: DIContainer.resolve(type: Interceptor.self, name: "tokenInterceptor"), + authInterceptor: DIContainer.resolve(type: Interceptor.self, name: "authInterceptor") ) } DIContainer.register(type: TokenRepository.self) { @@ -168,19 +173,19 @@ extension AppDelegate { DIContainer.register(type: DictionaryDetailAPIRepository.self) { DictionaryDetailAPIRepositoryImpl( provider: DIContainer.resolve(type: NetworkProvider.self), - tokenInterceptor: DIContainer.resolve(type: Interceptor.self) + tokenInterceptor: DIContainer.resolve(type: Interceptor.self, name: "tokenInterceptor") ) } DIContainer.register(type: DictionaryListAPIRepository.self) { DictionaryListAPIRepositoryImpl( provider: DIContainer.resolve(type: NetworkProvider.self), - tokenInterceptor: DIContainer.resolve(type: Interceptor.self) + tokenInterceptor: DIContainer.resolve(type: Interceptor.self, name: "tokenInterceptor") ) } DIContainer.register(type: BookmarkRepository.self) { BookmarkRepositoryImpl( provider: DIContainer.resolve(type: NetworkProvider.self), - interceptor: DIContainer.resolve(type: Interceptor.self) + interceptor: DIContainer.resolve(type: Interceptor.self, name: "tokenInterceptor") ) } DIContainer.register(type: UserDefaultsRepository.self) { @@ -189,13 +194,13 @@ extension AppDelegate { DIContainer.register(type: AlarmAPIRepository.self) { AlarmAPIRepositoryImpl( provider: DIContainer.resolve(type: NetworkProvider.self), - interceptor: DIContainer.resolve(type: Interceptor.self) + interceptor: DIContainer.resolve(type: Interceptor.self, name: "tokenInterceptor") ) } DIContainer.register(type: CollectionAPIRepository.self) { CollectionAPIRepositoryImpl( provider: DIContainer.resolve(type: NetworkProvider.self), - tokenInterceptor: DIContainer.resolve(type: Interceptor.self) + tokenInterceptor: DIContainer.resolve(type: Interceptor.self, name: "tokenInterceptor") ) } } diff --git a/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift b/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift deleted file mode 100644 index 607bcbc1..00000000 --- a/MLS/Presentation/BaseFeature/BaseFeature/Interface/EmptyViewState.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// public enum ViewType: Equatable { -// case dictionary(ViewState) -// case bookmark(ViewState) -// -// public enum ViewState { -// case login(data: Bool) -// case logout(data: Bool) -// } -// -// var mainText: String? { -// switch self { -// case let .bookmark(state): -// switch state { -// case let .login(data): -// return data ? nil : "아직 아무것도 없어요!" -// case let .logout(data): -// return "컬렉션은 로그인 후 이용 가능해요!" -// } -// case let .dictionary(state): -// switch state { -// case let .login(data): -// return data ? nil : "검색 결과가 없어요" -// case let .logout(data): -// return data ? nil : "검색 결과가 없어요" -// } -// } -// } -// -// var subText: String? { -// switch self { -// case let .bookmark(state): -// switch state { -// case let .login(data): -// return data ? nil : "아직 아무것도 없어요!" -// case let .logout(data): -// return "자주 보는 정보, 검색 없이 바로 확인 할 수 있어요." -// } -// case .dictionary(_): -// return nil -// } -// } -// -// var buttonText: String? { -// switch self { -// case let .bookmark(state): -// switch state { -// case let .login(data): -// return data ? nil : "북마크하러 가기" -// case let .logout(data): -// return "로그인하러 가기" -// } -// case .dictionary(_): -// return nil -// } -// } -// } From cf1cbe46abfcaf45e62f32da361df9ac3e7be843 Mon Sep 17 00:00:00 2001 From: p2glet Date: Fri, 12 Dec 2025 17:31:34 +0900 Subject: [PATCH 33/34] =?UTF-8?q?fix/#273:=20reissue=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/NetworkProviderImpl.swift | 31 +++++++++++++------ .../Domain/Interceptor/AuthInterceptor.swift | 25 +++++---------- .../AuthAPI/ReissueUseCaseImpl.swift | 26 ++++++++++++++-- MLS/MLS/Application/AppDelegate.swift | 9 ++++-- MLS/MLS/Application/SceneDelegate.swift | 14 ++------- .../DictionaryDetailBaseViewController.swift | 8 ++--- .../DictionaryDetailFactoryImpl.swift | 16 +++++----- 7 files changed, 71 insertions(+), 58 deletions(-) diff --git a/MLS/Data/Data/Providers/Network/NetworkProviderImpl.swift b/MLS/Data/Data/Providers/Network/NetworkProviderImpl.swift index 959eb5d0..f52e6e5d 100644 --- a/MLS/Data/Data/Providers/Network/NetworkProviderImpl.swift +++ b/MLS/Data/Data/Providers/Network/NetworkProviderImpl.swift @@ -129,28 +129,39 @@ private extension NetworkProviderImpl { /// - response: 상태코드를 포함한 통신 응답 /// - error: 통신간에 발생한 에러 /// - Returns: 유효성검사 결과에 따른 데이터와 에러 - func checkValidation(data: Data?, response: URLResponse?, error: Error?, interceptor: Interceptor?) -> Result { - if let interceptor = interceptor { - if interceptor.retry(data: data, response: response, error: error) { - return .failure(.retry) - } - } + func checkValidation( + data: Data?, + response: URLResponse?, + error: Error?, + interceptor: Interceptor? + ) -> Result { + + // 1️⃣ 네트워크 레벨 에러 먼저 체크 if let error { if let urlError = error as? URLError, urlError.code == .unsupportedURL { - return .failure(NetworkError.urlRequest(error)) + return .failure(.urlRequest(error)) } - return .failure(NetworkError.network(error)) + return .failure(.network(error)) } + // 2️⃣ HTTP 응답 객체 확인 guard let httpResponse = response as? HTTPURLResponse else { - return .failure(NetworkError.httpError) + return .failure(.httpError) } + // 3️⃣ 상태 코드 기반 검사 guard (200 ... 299).contains(httpResponse.statusCode) else { + // ❗️여기서만 인터셉터 개입 + if let interceptor = interceptor, + interceptor.retry(data: data, response: response, error: error) { + return .failure(.retry) + } + let errorMessage = data.flatMap { String(data: $0, encoding: .utf8) } ?? "Unknown" - return .failure(NetworkError.statusError(httpResponse.statusCode, errorMessage)) + return .failure(.statusError(httpResponse.statusCode, errorMessage)) } + // ✅ 성공 응답 return .success(data) } } diff --git a/MLS/Domain/Domain/Interceptor/AuthInterceptor.swift b/MLS/Domain/Domain/Interceptor/AuthInterceptor.swift index e9b3c50b..5496655d 100644 --- a/MLS/Domain/Domain/Interceptor/AuthInterceptor.swift +++ b/MLS/Domain/Domain/Interceptor/AuthInterceptor.swift @@ -13,11 +13,8 @@ public final class AuthInterceptor: Interceptor { public func adapt(_ request: URLRequest) -> URLRequest { var request = request - switch tokenRepository.fetchToken(type: .accessToken) { - case .success(let token): + if case .success(let token) = tokenRepository.fetchToken(type: .accessToken) { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - case .failure(let error): - print("Failed to fetch access token: \(error)") } return request } @@ -26,32 +23,24 @@ public final class AuthInterceptor: Interceptor { guard let httpResponse = response as? HTTPURLResponse, let url = httpResponse.url else { return false } - // 🚫 reissue 요청은 retry 금지 if url.path.contains("/auth/reissue") { print("⚠️ reissue 요청에서는 retry 하지 않음") return false } if httpResponse.statusCode == 401 { - switch tokenRepository.fetchToken(type: .refreshToken) { - case .success(let refreshToken): - let authRepo = authRepository() - authRepo.reissueToken(refreshToken: refreshToken) - .subscribe(onNext: { newTokens in - _ = self.tokenRepository.saveToken(type: .accessToken, value: newTokens.accessToken) - _ = self.tokenRepository.saveToken(type: .refreshToken, value: newTokens.refreshToken) - print("✅ 토큰 재발급 성공") + if case .success(let refreshToken) = tokenRepository.fetchToken(type: .refreshToken) { + let repo = authRepository() + repo.reissueToken(refreshToken: refreshToken) + .subscribe(onNext: { _ in + print("✅ reissue 완료 (저장은 UseCase 쪽에서 처리)") }, onError: { error in - print("❌ 토큰 재발급 실패: \(error)") + print("❌ reissue 실패: \(error)") }) .dispose() return true - case .failure(let error): - print("Failed to fetch refresh token: \(error)") - return false } } - return false } } diff --git a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/ReissueUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/ReissueUseCaseImpl.swift index 428df3b6..4f632e16 100644 --- a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/ReissueUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/ReissueUseCaseImpl.swift @@ -4,14 +4,34 @@ import DomainInterface import RxSwift -public class ReissueUseCaseImpl: ReissueUseCase { - private var repository: AuthAPIRepository +public final class ReissueUseCaseImpl: ReissueUseCase { + private let repository: AuthAPIRepository + private let tokenRepository: TokenRepository - public init(repository: AuthAPIRepository) { + public init( + repository: AuthAPIRepository, + tokenRepository: TokenRepository + ) { self.repository = repository + self.tokenRepository = tokenRepository } public func execute(refreshToken: String) -> Observable { return repository.reissueToken(refreshToken: refreshToken) + .flatMap { [weak self] response -> Observable in + guard let self = self else { return .empty() } + + let saveAccess = self.tokenRepository.saveToken(type: .accessToken, value: response.accessToken) + let saveRefresh = self.tokenRepository.saveToken(type: .refreshToken, value: response.refreshToken) + + switch (saveAccess, saveRefresh) { + case (.success, .success): + print("✅ 새 토큰 저장 완료") + return .just(response) + default: + print("❌ 토큰 저장 실패") + return .error(TokenRepositoryError.dataConversionError(message: "Failed to save new tokens")) + } + } } } diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index 532e6730..0ffa7712 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -280,7 +280,7 @@ extension AppDelegate { } DIContainer.register(type: ReissueUseCase.self) { ReissueUseCaseImpl( - repository: DIContainer.resolve(type: AuthAPIRepository.self) + repository: DIContainer.resolve(type: AuthAPIRepository.self), tokenRepository: DIContainer.resolve(type: TokenRepository.self) ) } DIContainer.register(type: PutFCMTokenUseCase.self) { @@ -669,6 +669,9 @@ extension AppDelegate { ) ) } + DIContainer.register(type: FetchVisitDictionaryDetailUseCase.self) { + FetchVisitDictionaryDetailUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } } fileprivate func registerFactory() { @@ -795,8 +798,8 @@ extension AppDelegate { setBookmarkUseCase: DIContainer.resolve( type: SetBookmarkUseCase.self ), - fetchVisitBookmarkUseCase: DIContainer.resolve( - type: FetchVisitBookmarkUseCase.self + fetchVisitDictionaryDetailUseCase: DIContainer.resolve( + type: FetchVisitDictionaryDetailUseCase.self ) ) } diff --git a/MLS/MLS/Application/SceneDelegate.swift b/MLS/MLS/Application/SceneDelegate.swift index 929185d5..7e183d01 100644 --- a/MLS/MLS/Application/SceneDelegate.swift +++ b/MLS/MLS/Application/SceneDelegate.swift @@ -41,25 +41,16 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { private func startScene(coordinator: AppCoordinatorProtocol) { let fetchTokenUseCase = DIContainer.resolve(type: FetchTokenFromLocalUseCase.self) let reissueUseCase = DIContainer.resolve(type: ReissueUseCase.self) - let saveTokenUseCase = DIContainer.resolve(type: SaveTokenToLocalUseCase.self) let fetchResult = fetchTokenUseCase.execute(type: .refreshToken) switch fetchResult { case .success(let refreshToken): - // ✅ refreshToken 존재 → accessToken 재발급 시도 reissueUseCase.execute(refreshToken: refreshToken) .observe(on: MainScheduler.instance) .subscribe( - onNext: { response in - let accessSave = saveTokenUseCase.execute(type: .accessToken, value: response.accessToken) - let refreshSave = saveTokenUseCase.execute(type: .refreshToken, value: response.refreshToken) - - if case .success = accessSave, case .success = refreshSave { - coordinator.showMainTab() - } else { - coordinator.showLogin(exitRoute: .home) - } + onNext: { _ in + coordinator.showMainTab() }, onError: { _ in coordinator.showLogin(exitRoute: .home) @@ -68,7 +59,6 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { .disposed(by: disposeBag) case .failure: - // ✅ refreshToken 없으면 바로 로그인으로 coordinator.showLogin(exitRoute: .home) } } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index 1d5b39f5..d37a776d 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift @@ -36,7 +36,7 @@ class DictionaryDetailBaseViewController: BaseViewController { private let detailOnBoardingFactory: DetailOnBoardingFactory private let appCoordinator: AppCoordinatorProtocol - private let fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase + private let fetchVisitDictionaryDetailUseCase: FetchVisitDictionaryDetailUseCase // MARK: - Components public var mainView = DictionaryDetailBaseView() @@ -51,7 +51,7 @@ class DictionaryDetailBaseViewController: BaseViewController { dictionaryDetailFactory: DictionaryDetailFactory, detailOnBoardingFactory: DetailOnBoardingFactory, appCoordinator: AppCoordinatorProtocol, - fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase, + fetchVisitDictionaryDetailUseCase: FetchVisitDictionaryDetailUseCase, bookmarkRelay: PublishRelay<(Int, Bool)>? ) { self.type = type @@ -60,7 +60,7 @@ class DictionaryDetailBaseViewController: BaseViewController { self.appCoordinator = appCoordinator self.dictionaryDetailFactory = dictionaryDetailFactory self.detailOnBoardingFactory = detailOnBoardingFactory - self.fetchVisitBookmarkUseCase = fetchVisitBookmarkUseCase + self.fetchVisitDictionaryDetailUseCase = fetchVisitDictionaryDetailUseCase mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) self.bookmarkRelay = bookmarkRelay super.init() @@ -346,7 +346,7 @@ extension DictionaryDetailBaseViewController { } func checkVisited() { - fetchVisitBookmarkUseCase.execute() + fetchVisitDictionaryDetailUseCase.execute() .withUnretained(self) .subscribe { owner, isVisit in if !isVisit { diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift index 56933afc..f07eeb2e 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift @@ -29,7 +29,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { private let dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase private let checkLoginUseCase: CheckLoginUseCase private let setBookmarkUseCase: SetBookmarkUseCase - private let fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase + private let fetchVisitDictionaryDetailUseCase: FetchVisitDictionaryDetailUseCase public init( loginFactory: @escaping () -> LoginFactory, @@ -52,7 +52,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase, checkLoginUseCase: CheckLoginUseCase, setBookmarkUseCase: SetBookmarkUseCase, - fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase + fetchVisitDictionaryDetailUseCase: FetchVisitDictionaryDetailUseCase ) { self.loginFactory = loginFactory self.bookmarkModalFactory = bookmarkModalFactory @@ -74,7 +74,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { self.setBookmarkUseCase = setBookmarkUseCase self.appCoordinator = appCoordinator self.dictionaryDetailFactory = dictionaryDetailFactory - self.fetchVisitBookmarkUseCase = fetchVisitBookmarkUseCase + self.fetchVisitDictionaryDetailUseCase = fetchVisitDictionaryDetailUseCase } public func make(type: DictionaryType, id: Int, bookmarkRelay: PublishRelay<(Int, Bool)>?) -> BaseViewController { @@ -91,7 +91,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, bookmarkRelay: bookmarkRelay ) let reactor = ItemDictionaryDetailReactor( @@ -111,7 +111,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, bookmarkRelay: bookmarkRelay ) let reactor = MonsterDictionaryDetailReactor( @@ -132,7 +132,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, bookmarkRelay: bookmarkRelay ) let reactor = MapDictionaryDetailReactor( @@ -153,7 +153,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, bookmarkRelay: bookmarkRelay ) let reactor = NpcDictionaryDetailReactor( @@ -174,7 +174,7 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { loginFactory: loginFactory(), dictionaryDetailFactory: dictionaryDetailFactory(), detailOnBoardingFactory: detailOnBoardingFactory, - appCoordinator: appCoordinator(), fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, bookmarkRelay: bookmarkRelay ) let reactor = QuestDictionaryDetailReactor( From 5a92278778f798be2edea0273f86a9de9cf7b553 Mon Sep 17 00:00:00 2001 From: p2glet Date: Fri, 12 Dec 2025 18:16:06 +0900 Subject: [PATCH 34/34] =?UTF-8?q?fix/#273:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=95=20=EC=9E=84=EC=8B=9C=20=EC=A4=91?= =?UTF-8?q?=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift b/MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift index 0a482051..56449af0 100644 --- a/MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/AlarmAPIRepositoryImpl.swift @@ -14,31 +14,31 @@ public class AlarmAPIRepositoryImpl: AlarmAPIRepository { } public func fetchPatchNotes(cursor: [Int]?, pageSize: Int) -> Observable> { - let endpoint = AlarmEndPoint.fetchPatchNotes(query: AlarmQuery(cursor: cursor, pageSize: pageSize)) + let endpoint = AlarmEndPoint.fetchPatchNotes(query: AlarmQuery(cursor: cursor, pageSize: 999/*pageSize*/)) return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor) .map { $0.toAlarmDomain() } } public func fetchNotices(cursor: [Int]?, pageSize: Int) -> Observable> { - let endpoint = AlarmEndPoint.fetchNotices(query: AlarmQuery(cursor: cursor, pageSize: pageSize)) + let endpoint = AlarmEndPoint.fetchNotices(query: AlarmQuery(cursor: cursor, pageSize: 999/*pageSize*/)) return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor) .map { $0.toAlarmDomain() } } public func fetchOutdatedEvents(cursor: [Int]?, pageSize: Int) -> Observable> { - let endpoint = AlarmEndPoint.fetchOutdatedEvents(query: AlarmQuery(cursor: cursor, pageSize: pageSize)) + let endpoint = AlarmEndPoint.fetchOutdatedEvents(query: AlarmQuery(cursor: cursor, pageSize: 999/*pageSize*/)) return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor) .map { $0.toAlarmDomain() } } public func fetchOngoingEvents(cursor: [Int]?, pageSize: Int) -> Observable> { - let endpoint = AlarmEndPoint.fetchOngoingEvents(query: AlarmQuery(cursor: cursor, pageSize: pageSize)) + let endpoint = AlarmEndPoint.fetchOngoingEvents(query: AlarmQuery(cursor: cursor, pageSize: 999/*pageSize*/)) return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor) .map { $0.toAlarmDomain() } } public func fetchAll(cursor: [Int]?, pageSize: Int) -> Observable> { - let endpoint = AlarmEndPoint.fetchAll(query: AlarmQuery(cursor: cursor, pageSize: pageSize)) + let endpoint = AlarmEndPoint.fetchAll(query: AlarmQuery(cursor: cursor, pageSize: 999/*pageSize*/)) return provider.requestData(endPoint: endpoint, interceptor: tokenInterceptor) .map { $0.toAllAlarmDomain() } }