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/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/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift b/MLS/Data/Data/Network/Endpoints/DictionaryDetailEndPoint.swift index 42471e1c..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) -> ResponsableEndPoint<[DictionaryDetailMapSpawnMonsterResponseDTO]> { - return .init(baseURL: base, path: "/api/v1/maps/\(id)/monsters", method: .GET) + 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/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/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() } } 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/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift b/MLS/Data/Data/Repository/DictionaryDetailAPIRepositoryImpl.swift index 51d601c0..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) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> { - let endPoint = DictionaryDetailEndPoint.fetchMapDetailSpawnMonster(id: id) + 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/Data/Data/Repository/UserDefaultsRepositoryImpl.swift b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift index f0e8f118..e8ea829f 100644 --- a/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift +++ b/MLS/Data/Data/Repository/UserDefaultsRepositoryImpl.swift @@ -6,6 +6,8 @@ import RxSwift public final class UserDefaultsRepositoryImpl: UserDefaultsRepository { private let recentSearchkey = "recentSearch" private let platformKey = "platformKey" + private let bookmarkkey = "bookmark" + private let dictionaryDetailkey = "dictionaryDetailkey" public init() {} @@ -67,4 +69,38 @@ 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() + } + } + + 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/Interceptor/AuthInterceptor.swift b/MLS/Domain/Domain/Interceptor/AuthInterceptor.swift new file mode 100644 index 00000000..5496655d --- /dev/null +++ b/MLS/Domain/Domain/Interceptor/AuthInterceptor.swift @@ -0,0 +1,46 @@ +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 + if case .success(let token) = tokenRepository.fetchToken(type: .accessToken) { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + 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 } + + if url.path.contains("/auth/reissue") { + print("⚠️ reissue 요청에서는 retry 하지 않음") + return false + } + + if httpResponse.statusCode == 401 { + 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("❌ reissue 실패: \(error)") + }) + .dispose() + return true + } + } + 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/CheckLoginUseCaseImpl.swift b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift index c3cd42df..5b285263 100644 --- a/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift +++ b/MLS/Domain/Domain/UseCaseImpl/AuthAPI/CheckLoginUseCaseImpl.swift @@ -1,8 +1,9 @@ import DomainInterface +import RxRelay import RxSwift -public class CheckLoginUseCaseImpl: CheckLoginUseCase { +public final class CheckLoginUseCaseImpl: CheckLoginUseCase { private let authRepository: AuthAPIRepository private let tokenRepository: TokenRepository @@ -14,6 +15,8 @@ public class CheckLoginUseCaseImpl: CheckLoginUseCase { public func execute() -> 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 +38,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/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/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/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/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 09f21ed0..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) -> 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/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/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/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/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/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryMainResponse.swift index 145358a1..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 @@ -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/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift b/MLS/Domain/DomainInterface/Entity/DictionaryList/DictionaryType.swift index 9fb29663..cb164b00 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 @@ -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 + .appearMapWithText, .dropItemWithText ] case .map: return [ - .mostAppear + .appearMonsterWithText, .appearNPC ] case .npc: return [ - .levelLowest, .levelHighest + .quest ] default: return [] @@ -103,4 +107,23 @@ public enum DictionaryType: 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/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/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/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift b/MLS/Domain/DomainInterface/Repository/DictionaryDetailAPIRepository.swift index 2fb8e73a..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) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> + func fetchMapDetailSpawnMonster(id: Int, sort: String?) -> Observable<[DictionaryDetailMapSpawnMonsterResponse]> // Map 디테일 출현 Npc 정보 func fetchMapDetailNpc(id: Int) -> Observable<[DictionaryDetailMapNpcResponse]> } diff --git a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift index b469b8c1..763c10d2 100644 --- a/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift +++ b/MLS/Domain/DomainInterface/Repository/UserDefaultsRepository.swift @@ -7,4 +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/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/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 47865647..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) -> 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/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/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/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index 01d3a82b..0ffa7712 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 @@ -153,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) { @@ -175,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) { @@ -196,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") ) } } @@ -282,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) { @@ -643,9 +641,6 @@ extension AppDelegate { ) ) } - // DIContainer.register(type: AddCollectionsToBookmarkUseCase.self) { - // AddCollectionsToBookmarkUseCaseImpl(repository: DIContainer.resolve(type: CollectionAPIRepository.self)) - // } DIContainer.register(type: SetCollectionUseCase.self) { SetCollectionUseCaseImpl( repository: DIContainer.resolve( @@ -660,9 +655,6 @@ extension AppDelegate { ) ) } - // DIContainer.register(type: AddBookmarksToCollectionUseCase.self) { - // AddBookmarksToCollectionUseCaseImpl(repository: DIContainer.resolve(type: CollectionAPIRepository.self)) - // } DIContainer.register(type: AddCollectionAndBookmarkUseCase.self) { AddCollectionAndBookmarkUseCaseImpl( repository: DIContainer.resolve( @@ -670,6 +662,16 @@ extension AppDelegate { ) ) } + DIContainer.register(type: FetchVisitBookmarkUseCase.self) { + FetchVisitBookmarkUseCaseImpl( + repository: DIContainer.resolve( + type: UserDefaultsRepository.self + ) + ) + } + DIContainer.register(type: FetchVisitDictionaryDetailUseCase.self) { + FetchVisitDictionaryDetailUseCaseImpl(repository: DIContainer.resolve(type: UserDefaultsRepository.self)) + } } fileprivate func registerFactory() { @@ -745,6 +747,9 @@ extension AppDelegate { DIContainer .resolve(type: DictionaryDetailFactory.self) }, + detailOnBoardingFactory: DIContainer.resolve( + type: DetailOnBoardingFactory.self + ), appCoordinator: { DIContainer.resolve(type: AppCoordinatorProtocol.self) }, @@ -792,6 +797,9 @@ extension AppDelegate { ), setBookmarkUseCase: DIContainer.resolve( type: SetBookmarkUseCase.self + ), + fetchVisitDictionaryDetailUseCase: DIContainer.resolve( + type: FetchVisitDictionaryDetailUseCase.self ) ) } @@ -896,17 +904,18 @@ extension AppDelegate { } DIContainer.register(type: DictionaryMainViewFactory.self) { DictionaryMainViewFactoryImpl( - dictionaryMainListFactory: - DIContainer - .resolve(type: DictionaryMainListFactory.self), + dictionaryMainListFactory: DIContainer.resolve( + type: DictionaryMainListFactory.self + ), searchFactory: DIContainer.resolve( type: DictionarySearchFactory.self ), - notificationFactory: - DIContainer - .resolve(type: DictionaryNotificationFactory.self), - checkLoginUseCase: DIContainer.resolve( - type: CheckLoginUseCase.self + notificationFactory: DIContainer.resolve( + type: DictionaryNotificationFactory.self + ), + loginFactory: DIContainer.resolve(type: LoginFactory.self), + fetchProfileUseCase: DIContainer.resolve( + type: FetchProfileUseCase.self ) ) } @@ -999,6 +1008,12 @@ extension AppDelegate { setBookmarkUseCase: DIContainer .resolve(type: SetBookmarkUseCase.self), + checkLoginUseCase: + DIContainer + .resolve(type: CheckLoginUseCase.self), + fetchVisitBookmarkUseCase: + DIContainer + .resolve(type: FetchVisitBookmarkUseCase.self), onBoardingFactory: DIContainer .resolve(type: BookmarkOnBoardingFactory.self), @@ -1013,7 +1028,8 @@ extension AppDelegate { .resolve(type: DictionarySearchFactory.self), notificationFactory: DIContainer.resolve( type: DictionaryNotificationFactory.self - ) + ), + loginFactory: DIContainer.resolve(type: LoginFactory.self) ) } DIContainer.register(type: BookmarkOnBoardingFactory.self) { @@ -1043,8 +1059,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 @@ -1151,6 +1167,8 @@ extension AppDelegate { } DIContainer.register(type: CustomerSupportFactory.self) { CustomerSupportBaseViewFactoryImpl( + policyFactory: DIContainer.resolve( + type: PolicyFactory.self), fetchNoticesUseCase: DIContainer.resolve( type: FetchNoticesUseCase.self ), @@ -1209,5 +1227,11 @@ extension AppDelegate { ) ) } + DIContainer.register(type: DetailOnBoardingFactory.self) { + DetailOnBoardingFactoryImpl() + } + DIContainer.register(type: PolicyFactory.self) { + PolicyFactoryImpl() + } } } 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/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/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 00000000..8cd87b9d Binary files /dev/null and b/MLS/MLS/Resource/Settings.bundle/en.lproj/Root.strings differ 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/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..08566c23 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoadingNotification/OnBoardingNotificationViewController.swift @@ -77,6 +77,11 @@ public extension OnBoardingNotificationViewController { .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 } .bind(to: reactor.action) @@ -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..ba30890a 100644 --- a/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift +++ b/MLS/Presentation/AuthFeature/AuthFeature/OnBoardingNotificationSheet/OnBoardingNotificationSheetViewController.swift @@ -61,10 +61,18 @@ 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 } .bind(to: reactor.action) @@ -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/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/SharedView/BaseListView.swift b/MLS/Presentation/BaseFeature/BaseFeature/Shared/BaseListView.swift similarity index 93% rename from MLS/Presentation/BaseFeature/BaseFeature/SharedView/BaseListView.swift rename to MLS/Presentation/BaseFeature/BaseFeature/Shared/BaseListView.swift index 033df7e2..bfad802d 100644 --- a/MLS/Presentation/BaseFeature/BaseFeature/SharedView/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/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/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/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) } diff --git a/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift b/MLS/Presentation/BaseFeature/BaseFeature/UICollectionReusableViews/CollectionViewCells/DictionaryListCell.swift index 21c7d543..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 { @@ -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/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/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/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/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/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift b/MLS/Presentation/BaseFeature/BaseFeature/Utills/NotificationPermissionManager.swift new file mode 100644 index 00000000..6665635e --- /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 + ) { + 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/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/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/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 4c3f4973..d0ec0e6c 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 { @@ -27,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, @@ -136,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) @@ -178,7 +180,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) @@ -187,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) @@ -206,16 +209,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..8d1aaa9c 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,41 @@ import DomainInterface public final class BookmarkMainFactoryImpl: BookmarkMainFactory { private let setBookmarkUseCase: SetBookmarkUseCase + private let checkLoginUseCase: CheckLoginUseCase + private let fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase + 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, + fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase, onBoardingFactory: BookmarkOnBoardingFactory, bookmarkListFactory: BookmarkListFactory, collectionListFactory: CollectionListFactory, searchFactory: DictionarySearchFactory, - notificationFactory: DictionaryNotificationFactory + notificationFactory: DictionaryNotificationFactory, + loginFactory: LoginFactory ) { self.setBookmarkUseCase = setBookmarkUseCase + self.checkLoginUseCase = checkLoginUseCase + self.fetchVisitBookmarkUseCase = fetchVisitBookmarkUseCase 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, fetchVisitBookmarkUseCase: fetchVisitBookmarkUseCase ) let viewController = BookmarkMainViewController( onBoardingFactory: onBoardingFactory, @@ -37,6 +48,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..1c9d5da2 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,38 +30,56 @@ 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 + private let fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase public var initialState: State private let disposeBag = DisposeBag() - public init(setBookmarkUseCase: SetBookmarkUseCase) { + public init(setBookmarkUseCase: SetBookmarkUseCase, checkLoginUseCase: CheckLoginUseCase, fetchVisitBookmarkUseCase: FetchVisitBookmarkUseCase) { self.initialState = State(route: .none) self.setBookmarkUseCase = setBookmarkUseCase + self.checkLoginUseCase = checkLoginUseCase + self.fetchVisitBookmarkUseCase = fetchVisitBookmarkUseCase } public func mutate(action: Action) -> Observable { switch action { - case .viewDidAppear: - return .empty() - case .dismissOnboarding: - return .empty() + case .viewWillAppear: + 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: return Observable.just(.navigateTo(.notification)) + case .loginButtonTapped: + return .just(.navigateTo(.login)) } } 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 } return newState } diff --git a/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainView.swift index e2709211..f58e0cfa 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,44 +41,27 @@ 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() } + addSubview(tabCollectionView) + addSubview(pageViewController.view) + addSubview(emptyView) + tabCollectionView.snp.makeConstraints { make in make.top.equalTo(headerView.snp.bottom).offset(Constant.topMargin) make.horizontalEdges.equalToSuperview() @@ -91,6 +73,29 @@ private extension BookmarkMainView { make.horizontalEdges.equalTo(safeAreaLayoutGuide) make.bottom.equalToSuperview().inset(Constant.bottomTabHeight) } + + 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/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift b/MLS/Presentation/BookmarkFeature/BookmarkFeature/BookmarkMain/BookmarkMainViewController.swift index 1352a597..8c36fc05 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,26 @@ 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) + owner.underLineController.setHidden(hidden: true) + } + .disposed(by: disposeBag) + } } 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/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/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..add5f4c0 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 @@ -159,7 +163,7 @@ extension CollectionEditViewController: UICollectionViewDelegate, UICollectionVi } cell.inject( - type: .bookmark, + type: .checkbox, input: DictionaryListCell.Input( type: item.type, mainText: item.name, diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift b/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift index c2744498..738acf56 100644 --- a/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift +++ b/MLS/Presentation/DesignSystem/DesignSystem/Components/CommonButton.swift @@ -175,4 +175,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/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/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 00000000..c7fa2f9f Binary files /dev/null and b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideAlert.imageset/guideAlert.png differ 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 00000000..07e674cb Binary files /dev/null and b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow1.imageset/guideArrow1.png differ 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 00000000..2b33ee8b Binary files /dev/null and b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideArrow2.imageset/guideArrow2.png differ 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 00000000..d92f19e2 Binary files /dev/null and b/MLS/Presentation/DesignSystem/DesignSystem/Resource/Image.xcassets/BookmarkOnboarding/guideIcon.imageset/guideIcon.png differ 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 diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Utills/Extension/UIColor+.swift b/MLS/Presentation/DesignSystem/DesignSystem/Utills/Extension/UIColor+.swift index 806c6535..b3c62f6e 100644 --- a/MLS/Presentation/DesignSystem/DesignSystem/Utills/Extension/UIColor+.swift +++ b/MLS/Presentation/DesignSystem/DesignSystem/Utills/Extension/UIColor+.swift @@ -9,7 +9,7 @@ public extension UIColor { static let primary50 = UIColor(hexCode: "FFE6D8") static let primary25 = UIColor(hexCode: "FFF1E9") - static let secondary = UIColor(hexCode: "FF9B56") + static let secondary = UIColor(hexCode: "FFAA00") static let textColor = UIColor(hexCode: "1D1D1F") static let neutral900 = UIColor(hexCode: "313131") diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseView.swift index b40114f4..2336b3c8 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseView.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseView.swift @@ -44,7 +44,6 @@ class DictionaryDetailBaseView: UIView { /// header에 들어가 컴포넌트들 담을 컨테이너 뷰 public let headerView: UIView = { let view = UIView() - return view }() diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailBaseViewController.swift index 2a4455e0..d37a776d 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] = [] { @@ -32,23 +33,37 @@ class DictionaryDetailBaseViewController: BaseViewController { private let bookmarkModalFactory: BookmarkModalFactory private let loginFactory: LoginFactory public let dictionaryDetailFactory: DictionaryDetailFactory + private let detailOnBoardingFactory: DetailOnBoardingFactory private let appCoordinator: AppCoordinatorProtocol + private let fetchVisitDictionaryDetailUseCase: FetchVisitDictionaryDetailUseCase + // MARK: - Components public var mainView = DictionaryDetailBaseView() // 타입설정 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, + detailOnBoardingFactory: DetailOnBoardingFactory, + appCoordinator: AppCoordinatorProtocol, + fetchVisitDictionaryDetailUseCase: FetchVisitDictionaryDetailUseCase, + bookmarkRelay: PublishRelay<(Int, Bool)>? + ) { self.type = type self.bookmarkModalFactory = bookmarkModalFactory self.loginFactory = loginFactory self.appCoordinator = appCoordinator self.dictionaryDetailFactory = dictionaryDetailFactory + self.detailOnBoardingFactory = detailOnBoardingFactory + self.fetchVisitDictionaryDetailUseCase = fetchVisitDictionaryDetailUseCase mainView.titleLabel.attributedText = .makeStyledString(font: .sub_m_b, text: type.detailTitle) + self.bookmarkRelay = bookmarkRelay super.init() - isBottomTabbarHidden = true } @available(*, unavailable) @@ -92,6 +107,7 @@ private extension DictionaryDetailBaseViewController { func configureUI() { mainView.scrollView.delegate = self + checkVisited() } } @@ -140,9 +156,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) @@ -239,11 +254,6 @@ extension DictionaryDetailBaseViewController { } } - // 북마크 버튼 클릭 시 - func updateBookmarkButton(isBookmarked: Bool) { - // TODO: 북마크 버튼 누르면 이벤트 발생 - } - func didSelectMenuTab(index: Int) { // 인덱스 유효성 검사 guard index < contentViews.count else { return } @@ -259,6 +269,7 @@ extension DictionaryDetailBaseViewController { buttonTap: ControlEvent, currentItem: Observable, isLogin: @escaping () -> Bool, + id: @escaping (T) -> Int, imageUrl: @escaping (T) -> String?, backgroundColor: UIColor, isBookmarked: @escaping (T) -> Bool, @@ -285,8 +296,11 @@ extension DictionaryDetailBaseViewController { return } + let itemId = id(item) + if isBookmarked(item) { toggleBookmark(true) + self.bookmarkRelay?.accept((itemId, false)) SnackBarFactory.createSnackBar( type: .delete, imageUrl: imageUrl(item), @@ -297,6 +311,7 @@ extension DictionaryDetailBaseViewController { ) } else { toggleBookmark(false) + self.bookmarkRelay?.accept((itemId, true)) SnackBarFactory.createSnackBar( type: .normal, imageUrl: imageUrl(item), @@ -329,6 +344,20 @@ extension DictionaryDetailBaseViewController { } } } + + func checkVisited() { + fetchVisitDictionaryDetailUseCase.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 { @@ -368,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/DictionaryDetailFactoryImpl.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift index 7148e978..f07eeb2e 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/DictionaryDetailFactoryImpl.swift @@ -4,10 +4,13 @@ import BookmarkFeatureInterface import DictionaryFeatureInterface import DomainInterface +import RxCocoa + 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 @@ -26,11 +29,13 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { private let dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase private let checkLoginUseCase: CheckLoginUseCase private let setBookmarkUseCase: SetBookmarkUseCase + private let fetchVisitDictionaryDetailUseCase: FetchVisitDictionaryDetailUseCase public init( loginFactory: @escaping () -> LoginFactory, bookmarkModalFactory: BookmarkModalFactory, dictionaryDetailFactory: @escaping () -> DictionaryDetailFactory, + detailOnBoardingFactory: DetailOnBoardingFactory, appCoordinator: @escaping () -> AppCoordinatorProtocol, dictionaryDetailMapUseCase: FetchDictionaryDetailMapUseCase, dictionaryDetailMapSpawnMonsterUseCase: FetchDictionaryDetailMapSpawnMonsterUseCase, @@ -46,10 +51,12 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { dictionaryDetailMonsterDropItemUseCase: FetchDictionaryDetailMonsterItemsUseCase, dictionaryDetailMonsterMapUseCase: FetchDictionaryDetailMonsterMapUseCase, checkLoginUseCase: CheckLoginUseCase, - setBookmarkUseCase: SetBookmarkUseCase + setBookmarkUseCase: SetBookmarkUseCase, + fetchVisitDictionaryDetailUseCase: FetchVisitDictionaryDetailUseCase ) { self.loginFactory = loginFactory self.bookmarkModalFactory = bookmarkModalFactory + self.detailOnBoardingFactory = detailOnBoardingFactory self.dictionaryDetailMapUseCase = dictionaryDetailMapUseCase self.dictionaryDetailMapSpawnMonsterUseCase = dictionaryDetailMapSpawnMonsterUseCase self.dictionaryDetailMapNpcUseCase = dictionaryDetailMapNpcUseCase @@ -67,9 +74,10 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { self.setBookmarkUseCase = setBookmarkUseCase self.appCoordinator = appCoordinator self.dictionaryDetailFactory = dictionaryDetailFactory + self.fetchVisitDictionaryDetailUseCase = fetchVisitDictionaryDetailUseCase } - 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 +85,15 @@ 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(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, + bookmarkRelay: bookmarkRelay + ) let reactor = ItemDictionaryDetailReactor( dictionaryDetailItemUseCase: dictionaryDetailItemUseCase, dictionaryDetailItemDropMonsterUseCase: dictionaryDetailItemDropMonsterUseCase, @@ -89,7 +105,15 @@ 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(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, + bookmarkRelay: bookmarkRelay + ) let reactor = MonsterDictionaryDetailReactor( dictionaryDetailMonsterUseCase: dictionaryDetailMonsterUseCase, dictionaryDetailMonsterDropItemUseCase: dictionaryDetailMonsterDropItemUseCase, @@ -102,6 +126,15 @@ public final class DictionaryDetailFactoryImpl: DictionaryDetailFactory { viewController.reactor = reactor } case .map: + viewController = MapDictionaryDetailViewController( + type: .map, + bookmarkModalFactory: bookmarkModalFactory, + loginFactory: loginFactory(), + dictionaryDetailFactory: dictionaryDetailFactory(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, + bookmarkRelay: bookmarkRelay + ) let reactor = MapDictionaryDetailReactor( dictionaryDetailMapUseCase: dictionaryDetailMapUseCase, dictionaryDetailMapSpawnMonsterUseCase: dictionaryDetailMapSpawnMonsterUseCase, @@ -110,11 +143,19 @@ 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(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, + bookmarkRelay: bookmarkRelay + ) let reactor = NpcDictionaryDetailReactor( dictionaryDetailNpcUseCase: dictionaryDetailNpcUseCase, dictionaryDetailNpcQuestUseCase: dictionaryDetailNpcQuestUseCase, @@ -123,12 +164,19 @@ 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(), + detailOnBoardingFactory: detailOnBoardingFactory, + appCoordinator: appCoordinator(), fetchVisitDictionaryDetailUseCase: fetchVisitDictionaryDetailUseCase, + bookmarkRelay: bookmarkRelay + ) let reactor = QuestDictionaryDetailReactor( dictionaryDetailQuestUseCase: dictionaryDetailQuestUseCase, dictionaryDetailQuestLinkedQuestUseCase: dictionaryDetailQuestLinkedQuestsUseCase, 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/Item/ItemDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift index c3a7c7e3..27b25de1 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Item/ItemDictionaryDetailViewController.swift @@ -36,9 +36,9 @@ private extension ItemDictionaryDetailViewController { // descriptionText detailInfoView.descriptionLabel.text = infos.descriptionText ?? "" + detailInfoView.reset() 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 +95,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 +128,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())") } } } @@ -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,17 +211,18 @@ 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)) } - owner.tabBarController?.presentModal(viewController) + owner.tabBarController?.presentModal(viewController, hideTabBar: true) 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) } } @@ -230,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 }, @@ -244,7 +247,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/Map/MapDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailReactor.swift index 91afbae1..cc0a12b1 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: 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/Map/MapDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift index 67881480..da01bef7 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Map/MapDictionaryDetailViewController.swift @@ -54,9 +54,10 @@ private extension MapDictionaryDetailViewController { func setUpMonsterView() { guard let reactor = reactor, - let filter = reactor.currentState.type.detailSortedFilter.first else { return } + 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 { @@ -64,15 +65,22 @@ private extension MapDictionaryDetailViewController { } 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)")) + 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.detailSortedFilter.first else { return } - appearNpcView.initFilter(firstFilter: filter) + guard let reactor = reactor else { return } + let npcs = reactor.currentState.npcs + appearNpcView.reset() contentViews.append(appearNpcView) if npcs.isEmpty { contentViews[2] = DetailEmptyView(type: .appearNPC) @@ -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) @@ -166,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 }, @@ -181,18 +191,18 @@ extension MapDictionaryDetailViewController { .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 + case .filter(let sort): + let viewController = owner.sortedFactory.make(sortedOptions: sort, selectedIndex: owner.selectedIndex) { index in owner.selectedIndex = index - let selectedFilter = reactor.currentState.type.detailSortedFilter[index] + let selectedFilter = reactor.currentState.monsterFilter[index] owner.appearMonsterView.selectFilter(selectedType: selectedFilter) reactor.action.onNext(.selectFilter(selectedFilter)) } - owner.tabBarController?.presentModal(viewController) + owner.tabBarController?.presentModal(viewController, hideTabBar: true) 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/MonsterDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailReactor.swift index b8315205..b355a781 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([ @@ -99,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 @@ -158,11 +156,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/Monster/MonsterDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Monster/MonsterDictionaryDetailViewController.swift index c7fdca31..c386e132 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.detailSortedFilter.first else { return } + 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.detailSortedFilter[selectedIndex] - dropItemView.selectFilter(selectedType: selectedFilter) - - isBottomTabbarHidden = true - reactor.state.map(\.monsterDetailInfo) .distinctUntilChanged() .observe(on: MainScheduler.instance) @@ -181,26 +188,25 @@ extension MonsterDictionaryDetailViewController { .withUnretained(self) .subscribe { owner, route in switch route { - case .filter(let type): + case .filter(let type, let sort): let selectedIndex = (type == .item) ? owner.dropItemSelectedIndex : owner.mapSelectedIntdex - let viewController = owner.sortedFactory.make(sortedOptions: type.detailSortedFilter, selectedIndex: selectedIndex) { index in + let viewController = owner.sortedFactory.make(sortedOptions: sort, selectedIndex: selectedIndex) { index in if type == .item { owner.dropItemSelectedIndex = index - let selectedFilter = type.detailSortedFilter[index] + let selectedFilter = sort[index] owner.dropItemView.selectFilter(selectedType: selectedFilter) reactor.action.onNext(.selectFilter(selectedFilter)) - } else if type == .map { owner.mapSelectedIntdex = index - let selectedFilter = type.detailSortedFilter[index] + let selectedFilter = sort[index] owner.appearMapView.selectFilter(selectedType: selectedFilter) + reactor.action.onNext(.selectFilter(selectedFilter)) } - owner.isBottomTabbarHidden = true } - owner.tabBarController?.presentModal(viewController) + 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 @@ -212,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/NpcDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailReactor.swift index 8d42be45..d643633f 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) }, @@ -90,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 diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/NPC/NpcDictionaryDetailViewController.swift index abe46a33..b5aef1f7 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,10 +112,6 @@ 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) } // 값이 바뀔때만 이벤트 받음 @@ -117,15 +119,15 @@ extension NpcDictionaryDetailViewController { .subscribe { owner, route in switch route { case .filter(let type): - let viewController = owner.sortedFactory.make(sortedOptions: type.detailSortedFilter, selectedIndex: owner.selectedIndex) { index in + let viewController = owner.sortedFactory.make(sortedOptions: type, selectedIndex: owner.selectedIndex) { index in owner.selectedIndex = index - let selectedFilter = reactor.currentState.type.detailSortedFilter[index] + let selectedFilter = type[index] owner.questView.selectFilter(selectedType: selectedFilter) reactor.action.onNext(.selectFilter(selectedFilter)) } - owner.tabBarController?.presentModal(viewController) + 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 @@ -165,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/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..798bcbc8 --- /dev/null +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/OnBoarding/DetailOnBoardingView.swift @@ -0,0 +1,192 @@ +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 = 48 + static let arrowTrailing: CGFloat = 28 + static let arrowMargin: CGFloat = 6 + 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) + 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) + 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.arrowMargin) + make.centerX.equalTo(firstLabel) + make.size.equalTo(Constant.arrowSize) + } + + secondLabel.snp.makeConstraints { make in + make.top.equalTo(secondArrow.snp.bottom).offset(Constant.arrowMargin) + 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/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift index 61395512..42a0835f 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailReactor.swift @@ -3,10 +3,21 @@ 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) - case detail(id: Int) + case detail(type: DictionaryType, id: Int) } public enum Action { @@ -14,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 { @@ -30,6 +42,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 +88,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 +135,12 @@ 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(type: .quest, id: id))) + case let .infoTapped(type: type, id: id): + return .just(.toNavigate(.detail(type: type, id: id))) } } @@ -142,15 +149,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..3eea7f99 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/Quest/QuestDictionaryDetailViewController.swift @@ -35,19 +35,33 @@ private extension QuestDictionaryDetailViewController { contentViews.append(detailInfoView) // 뭘로 빈페이지 보여줄지 정하지.. + detailInfoView.reset() if !(detailInfos.startNpcName == nil) { contentViews.append(detailInfoView) // 완료조건 추가 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: {} + ) + } } } } @@ -75,13 +89,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 { @@ -98,17 +112,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 +169,7 @@ extension QuestDictionaryDetailViewController { }) .disposed(by: disposeBag) - reactor.state.map(\.linkedQuestInfo) + reactor.state.map(\.totalQuest) .distinctUntilChanged() .observe(on: MainScheduler.instance) .withUnretained(self) @@ -159,6 +182,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 }, @@ -174,8 +198,8 @@ extension QuestDictionaryDetailViewController { .withUnretained(self) .subscribe { owner, route in switch route { - case .detail(let id): - let viewController = owner.dictionaryDetailFactory.make(type: .quest, id: id) + 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 @@ -184,21 +208,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/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) } diff --git a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryDetail/SectionStackView/DetailStackInfoView.swift index 4ec00849..00670a7f 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 = { @@ -209,6 +212,23 @@ extension DetailStackInfoView { 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, mainText: String, @@ -238,10 +258,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 +293,4 @@ extension DetailStackInfoView { make.height.equalTo(Constant.dividerHeight) } } - - @objc private func handleTap() { - onTap?() - } } 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/DictionaryListReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListReactor.swift index 621a2c83..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,15 +23,13 @@ open class DictionaryListReactor: Reactor { case itemFilterOptionSelected([(String, String)]) case setCurrentPage case fetchList - case fetchListFilter case undoLastDeletedBookmark + case toggleBookmark(id: Int, isSelected: Bool) } // MARK: - Mutation public enum Mutation { - case setListItem(DictionaryMainResponse) - case setFilterMonsterItem(DictionaryMainResponse) - case setFilterItemsItem(DictionaryMainResponse) + case setListItem(DictionaryMainResponse, updateBookmarkOnly: Bool = false) case showSortFilter case showFilter case setSort(String) @@ -40,6 +40,8 @@ open class DictionaryListReactor: Reactor { case setLastDeletedBookmark(DictionaryMainItemResponse?) case setJobId([Int]) case setCategoryId([Int]) + case updateBookmarkState(id: Int, isSelected: Bool) + case updateBookmarkStates([Int: Bool]) // 새 Mutation: 여러 북마크 반영 } // MARK: - State @@ -60,6 +62,7 @@ open class DictionaryListReactor: Reactor { var isLogin: Bool var lastDeletedBookmark: DictionaryMainItemResponse? + var isBookmarkUpdateOnly: Bool = false } public var initialState: State @@ -108,55 +111,91 @@ 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, - isFilter: true - ) - + return fetchList(sort: currentState.sort, startLevel: currentState.startLevel, endLevel: currentState.endLevel) case .undoLastDeletedBookmark: return handleUndoLastDeletedBookmark() - - case .itemFilterOptionSelected(let results): + case let .toggleBookmark(id, isSelected): + return handleToggleBookmark(id: id, isSelected: isSelected) + case let .itemFilterOptionSelected(results): return handleItemFilterOptionSelected(results: results) } } - // MARK: - Fetch - private func fetchList( - sort: String?, - startLevel: Int?, - endLevel: Int?, - isFilter: Bool = false - ) -> Observable { + // 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, updateBookmarkOnly): + newState.isBookmarkUpdateOnly = updateBookmarkOnly + newState.totalCounts = items.totalElements + 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 { + 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 + 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 }) { + newState.listItems[index].bookmarkId = isSelected ? (newState.listItems[index].bookmarkId ?? -1) : nil + } + case let .updateBookmarkStates(dict): + for index in 0.. Observable { let response: Observable switch currentState.type { @@ -172,7 +211,6 @@ open class DictionaryListReactor: Reactor { maxLevel: endLevel ) ) - case .item: response = dictionaryItemListUseCase.execute( keyword: currentState.keyword ?? "", @@ -184,7 +222,6 @@ open class DictionaryListReactor: Reactor { size: 20, sort: sort ) - case .map: response = dictionaryMapListUseCase.execute( keyword: currentState.keyword ?? "", @@ -192,7 +229,6 @@ open class DictionaryListReactor: Reactor { size: 20, sort: sort ?? "ASC" ) - case .npc: response = dictionaryNpcListUseCase.execute( keyword: currentState.keyword ?? "", @@ -200,7 +236,6 @@ open class DictionaryListReactor: Reactor { size: 20, sort: sort ?? "ASC" ) - case .quest: response = dictionaryQuestListUseCase.execute( keyword: currentState.keyword ?? "", @@ -208,108 +243,73 @@ open class DictionaryListReactor: Reactor { size: 20, sort: sort ?? "ASC" ) - case .total: response = dictionaryAllListUseCase.execute( keyword: currentState.keyword ?? "", page: currentState.currentPage ) - default: 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) + return response.map { response in + if updateBookmarkOnly { + 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 } } + var newResponse = response + newResponse.contents = merged + return .setListItem(newResponse, updateBookmarkOnly: true) } else { - return .setListItem(res) + return .setListItem(response) } } } - // 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) } - - 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]) } 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 - - let saveDeletedMutation: Observable = isSelected - ? .just(.setLastDeletedBookmark(bookmarkItem)) - : .just(.setLastDeletedBookmark(nil)) + guard let index = currentState.listItems.firstIndex(where: { $0.id == id }) else { return .empty() } + let targetItem = currentState.listItems[index] + + 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)) + } - 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) - ]) - ) + // 서버 호출 + bookmark 확정 + let api = setBookmarkUseCase.execute( + 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, + updateBookmarkOnly: true + )) + .observe(on: MainScheduler.asyncInstance) + + return .concat([optimistic, api]) } func handleSortOptionSelected(sort: SortType) -> Observable { @@ -317,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 { @@ -326,28 +330,24 @@ 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, - isFilter: true - ) + return self.fetchList(sort: self.currentState.sort, startLevel: startLevel, endLevel: endLevel) }) } 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)) + .observe(on: MainScheduler.asyncInstance) + + let apiCall = 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))) + .observe(on: MainScheduler.asyncInstance) + + return .concat([optimistic, apiCall]) } func handleItemFilterOptionSelected(results: [(String, String)]) -> Observable { @@ -360,12 +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, - isFilter: true - ) + return self.fetchList(sort: self.currentState.sort, startLevel: self.currentState.startLevel, endLevel: self.currentState.endLevel) }) } } 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 e1b6330d..0fed74a2 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryList/DictionaryListViewController.swift @@ -23,6 +23,8 @@ 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 @@ -123,19 +125,6 @@ extension DictionaryListViewController { .bind(to: itemCountRelay) .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 - // 보여줄 item이 없을 경우, 터치를 막는데 왜 막는건지? - // 몬스터나 아이템 탭에서 필터링을 하다가 item이 없을 경우, 필터 버튼도 터치가 안되서 계속 item 없음 - // self?.mainView.isUserInteractionEnabled = !item.isEmpty - }) - .disposed(by: disposeBag) - rx.viewWillAppear .map { _ in Reactor.Action.viewWillAppear } .bind(to: reactor.action) @@ -156,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): @@ -195,6 +183,47 @@ 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: id, isSelected: isBookmarked)) + }) + .disposed(by: disposeBag) + + // 기존 bookmarkChangeRelay 사용 대신 + reactor.state.map(\.listItems) + .observe(on: MainScheduler.instance) + .withUnretained(self) + .observe(on: MainScheduler.instance) + .bind { owner, items in + let collectionView = owner.mainView.listCollectionView + owner.mainView.checkEmptyData(isEmpty: items.isEmpty) + + guard let reactor = owner.reactor else { return } + if reactor.currentState.currentPage == 0, !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) } } @@ -218,7 +247,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 +261,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(id: item.id, isSelected: isSelected)) + + if isSelected { SnackBarFactory.createSnackBar( type: .delete, imageUrl: item.imageUrl, @@ -253,13 +279,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, @@ -267,27 +290,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 viewController = self.bookmarkModalFactory.make(bookmarkIds: [id], onComplete: { isAdd in - if isAdd { - ToastFactory.createToast( - message: - "컬렉션에 추가되었어요. 북마크 탭에서 확인 할 수 있어요." - ) + self.reactor?.state.map(\.listItems) + .compactMap { list in + list.first(where: { $0.id == item.id })?.bookmarkId + } + .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) }) - - viewController.modalPresentationStyle = .pageSheet - if let sheet = viewController.sheetPresentationController { - sheet.detents = [.medium(), .large()] - sheet.prefersGrabberVisible = true - sheet.preferredCornerRadius = 16 - } - self.present(viewController, animated: true) - } + .disposed(by: self.disposeBag) } ) } @@ -297,9 +322,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 @@ -309,24 +332,28 @@ 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) } 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/DictionaryMain/DictionaryMainReactor.swift b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift index 54286068..49a8a7fa 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryMain/DictionaryMainReactor.swift @@ -12,12 +12,16 @@ public final class DictionaryMainReactor: Reactor { } public enum Action { + case viewWillAppear case searchButtonTapped case notificationButtonTapped + case changeTab(Int) } public enum Mutation { case navigateTo(Route) + case setLogin(Bool) + case setCurrentTab(oldIndex: Int, newIndex: Int) } public struct State { @@ -26,34 +30,37 @@ public final class DictionaryMainReactor: Reactor { var sections: [String] { return type.pageTabList.map { $0.title } } + var isLogin = false + var currentPageIndex = 0 + var oldPageIndex = 0 } // 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) } + .catchAndReturn(.setLogin(false)) 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)) + case let .changeTab(index): + let oldIndex = currentState.currentPageIndex + return .just(.setCurrentTab(oldIndex: oldIndex, newIndex: index)) } } @@ -63,6 +70,11 @@ public final class DictionaryMainReactor: Reactor { switch mutation { case .navigateTo(let route): newState.route = route + case let .setLogin(isLogin): + newState.isLogin = isLogin + 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 dd969c9c..c4c1a01c 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 @@ -9,17 +10,18 @@ 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 + 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 @@ -57,6 +61,7 @@ public extension DictionaryMainViewController { setupConstraints() configureUI() setInitialIndex() + DictionaryTabRegistry.register(controller: self) } } @@ -110,6 +115,26 @@ private extension DictionaryMainViewController { self?.underLineController.setInitialIndicator() } } + + func moveToTab(oldIndex: Int, newIndex: Int) { + guard newIndex < viewControllers.count else { return } + let direction: UIPageViewController.NavigationDirection = newIndex > oldIndex ? .forward : .reverse + + mainView.pageViewController.setViewControllers( + [viewControllers[newIndex]], + direction: direction, + animated: true, + completion: nil + ) + + mainView.tabCollectionView.selectItem( + at: IndexPath(item: newIndex, section: 0), + animated: true, + scrollPosition: .centeredHorizontally + ) + + underLineController.animateIndicatorToSelectedItem() + } } // MARK: - Bind @@ -120,6 +145,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,10 +164,10 @@ 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) - .subscribe { (owner, route) in + .withUnretained(self) + .subscribe { owner, route in switch route { case .search: let controller = owner.searchFactory.make() @@ -145,11 +175,31 @@ 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 } } .disposed(by: disposeBag) + + reactor.state + .map(\.currentPageIndex) + .distinctUntilChanged() + .skip(1) + .withUnretained(self) + .subscribe(onNext: { owner, newIndex in + let oldIndex = reactor.currentState.oldPageIndex + owner.moveToTab(oldIndex: oldIndex, newIndex: newIndex) + }) + .disposed(by: disposeBag) + } +} + +public extension DictionaryMainViewController { + func changeTab(index: Int) { + reactor?.action.onNext(.changeTab(index)) } } @@ -170,9 +220,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)) } } } @@ -191,26 +239,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)) } } 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..0a85b474 100644 --- a/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift +++ b/MLS/Presentation/DictionaryFeature/DictionaryFeature/DictionaryNotification/DictionaryNotificationViewController.swift @@ -15,6 +15,8 @@ public final class DictionaryNotificationViewController: BaseViewController, Vie // MARK: - Properties public var disposeBag = DisposeBag() + private var lastPagingTime: Date = .distantPast + private var notificationSettingFactory: NotificationSettingFactory // MARK: - Components @@ -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 @@ -124,7 +125,19 @@ public extension DictionaryNotificationViewController { .subscribe { owner, _ in owner.mainView.notificationCollectionView.reloadData() } - .disposed(by: disposeBag) } + .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) + } } // MARK: - Delegate @@ -141,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/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..cfc2ab71 --- /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) + } + + } +} 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 8faaa4f4..8a07698d 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() @@ -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 == 0 ? 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 = 0 } } .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) @@ -181,7 +177,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..b94640af 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) @@ -72,14 +72,23 @@ extension MonsterFilterBottomSheetViewController { .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 } 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 +} 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 } diff --git a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/CustomerSupportBaseViewController.swift index 8c89045d..1f3c19ef 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,23 @@ 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) + 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/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/Policy/TermsViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/TermsViewController.swift new file mode 100644 index 00000000..bf93585c --- /dev/null +++ b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Policy/TermsViewController.swift @@ -0,0 +1,16 @@ +import UIKit + +import MyPageFeatureInterface + +final class TermsViewController: CustomerSupportBaseViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + let items = PolicyType.allCases + + mainView.setMenuHidden(true) + mainView.changeSetupConstraints() + createTermsDetailItem(items: items.map { $0.title }) + } +} 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/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/MyPageFeature/CustomerSupport/Terms/TermsViewController.swift b/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsViewController.swift deleted file mode 100644 index 53a8da41..00000000 --- a/MLS/Presentation/MyPageFeature/MyPageFeature/CustomerSupport/Terms/TermsViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -import UIKit - -final class TermsViewController: CustomerSupportBaseViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - let items = [ - "서비스 이용약관", - "개인정보 처리방침", - "마케팅 정보 수신 동의", - "오픈소스 라이선스 보기" - ] - - mainView.setMenuHidden(true) - mainView.changeSetupConstraints() - createTermsDetailItem(items: items) - } -} 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 }() 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)) } 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 } 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..5edac989 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) 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..aa334654 --- /dev/null +++ b/MLS/Presentation/MyPageFeature/MyPageFeatureInterface/PolicyType.swift @@ -0,0 +1,40 @@ +import Foundation + +public enum PolicyType: CaseIterable { + case service + case privacy + case openSource + + public var title: String { + switch self { + case .service: + "서비스 이용약관" + case .privacy: + "개인정보 처리방침" + case .openSource: + "오픈소스 라이선스" + } + } + + public var fileName: String { + switch self { + case .service: + "TermsOfService.txt" + case .privacy: + "PrivacyPolicy.txt" + case .openSource: + "" + } + } + + 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)" + } + } +}