diff --git a/.gitignore b/.gitignore index 7cf52ed8..de791f91 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,5 @@ fastlane/report.xml fastlane/README.md Projects/App/Resources/APIKey.xcconfig graph.png +CLAUDE.md +.claude diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index f9a4d5a7..be5d2f50 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -8,7 +8,7 @@ let project = Project( ), settings: .settings( base: .init() - .marketingVersion("2.2.0") + .marketingVersion("2.2.1") .swiftVersion("5.7") .currentProjectVersion("1") .appleGenericVersioningSystem() @@ -118,11 +118,11 @@ let project = Project( configurations: [ .debug(name: "Debug", settings: [ "CODE_SIGN_IDENTITY": "Apple Development: Nayeon Gu (3CMPGMMD7L)", - "PROVISIONING_PROFILE_SPECIFIER": "match Development com.yapp.moneymong 1769511003" + "PROVISIONING_PROFILE_SPECIFIER": "match Development com.yapp.moneymong" ]), .release(name: "Release", settings: [ "CODE_SIGN_IDENTITY": "Apple Distribution: Nayeon Gu (H5G7RFWFSQ)", - "PROVISIONING_PROFILE_SPECIFIER": "match AppStore com.yapp.moneymong 1769511386" + "PROVISIONING_PROFILE_SPECIFIER": "match AppStore com.yapp.moneymong" ]) ] ), diff --git a/Projects/App/Resources/App.entitlements b/Projects/App/Resources/App.entitlements index 4ea3fcaa..a8cfd6de 100644 --- a/Projects/App/Resources/App.entitlements +++ b/Projects/App/Resources/App.entitlements @@ -2,6 +2,11 @@ + com.apple.developer.associated-domains + + applinks:dev.moneymong.site + applinks:prod.moneymong.site + com.apple.developer.applesignin Default diff --git a/Projects/App/Sources/SceneDelegate.swift b/Projects/App/Sources/SceneDelegate.swift index d1f13cac..3a08919d 100644 --- a/Projects/App/Sources/SceneDelegate.swift +++ b/Projects/App/Sources/SceneDelegate.swift @@ -21,6 +21,7 @@ import Repository import SignFeature import User import UserInterface +import Utility class SceneDelegate: UIResponder, UIWindowSceneDelegate { let localStorage = LocalStorage() @@ -44,17 +45,32 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { appCoordinator?.start(animated: false) self.scene(scene, openURLContexts: connectionOptions.urlContexts) + + if let userActivity = connectionOptions.userActivities.first( + where: { $0.activityType == NSUserActivityTypeBrowsingWeb } + ) { + handleUniversalLink(from: userActivity) + } } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let url = URLContexts.first?.url else { return } if url.absoluteString.contains("widget://") { - DeepLinkManager.setDestination(url.absoluteString, agencyID: localStorage.selectedAgency) + let destination = url.absoluteString.replacingOccurrences(of: "widget://", with: "") + guard let agencyID = localStorage.selectedAgency else { return } + DeepLinkManager.setQuery([ + "destination": destination, + "agencyID": agencyID + ], notiName: .widgetLink) } else { KakaoAuthManager.shared.openURL(url) } } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + handleUniversalLink(from: userActivity) + } func sceneDidDisconnect(_ scene: UIScene) {} @@ -65,6 +81,23 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneWillEnterForeground(_ scene: UIScene) {} func sceneDidEnterBackground(_ scene: UIScene) {} + + private func handleUniversalLink(from userActivity: NSUserActivity) { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let url = userActivity.webpageURL, + let components = URLComponents(url: url, resolvingAgainstBaseURL: true), + url.path == Config.invitationPath else { return } + + let queryItems = components.queryItems ?? [] + guard let code = queryItems.first(where: { $0.name == "code" })?.value, + let agencyID = queryItems.first(where: { $0.name == "agencyID" })?.value, + !code.isEmpty, !agencyID.isEmpty else { return } + + DeepLinkManager.setQuery( + ["code": code, "agencyID": agencyID], + notiName: .invitationLink + ) + } } extension SceneDelegate { diff --git a/Projects/Core/MMNetwork/Sources/NetworkLogger.swift b/Projects/Core/MMNetwork/Sources/NetworkLogger.swift index 03d677b4..92d8406f 100644 --- a/Projects/Core/MMNetwork/Sources/NetworkLogger.swift +++ b/Projects/Core/MMNetwork/Sources/NetworkLogger.swift @@ -4,8 +4,6 @@ import OSLog import Alamofire final class NetworkLogger: EventMonitor { - let queue = DispatchQueue(label: "NetworkLogger") - private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "NetworkLogger") func request(_ request: DataRequest, didParseResponse response: DataResponse) where Value : Sendable { @@ -34,9 +32,9 @@ final class NetworkLogger: EventMonitor { log.append("\(response.data?.toPrettyPrintedString ?? "None")") if (200..<300) ~= statusCode { - logger.log("\(log)") + logger.log("\(log, privacy: .public)") } else { - logger.error("\(log)") + logger.error("\(log, privacy: .public)") } } diff --git a/Projects/Core/Repository/Sources/APIs/AgencyAPI/Repository/AgencyAPI.swift b/Projects/Core/Repository/Sources/APIs/AgencyAPI/Repository/AgencyAPI.swift index cb771c5d..36d9b1b3 100644 --- a/Projects/Core/Repository/Sources/APIs/AgencyAPI/Repository/AgencyAPI.swift +++ b/Projects/Core/Repository/Sources/APIs/AgencyAPI/Repository/AgencyAPI.swift @@ -1,6 +1,7 @@ import Foundation import MMNetworkInterface +import Utility enum AgencyAPI { case create(param: AgencyCreateRequestDTO) // 소속생성 diff --git a/Projects/Core/Repository/Sources/APIs/LedgerAPI/Repository/LedgerAPI.swift b/Projects/Core/Repository/Sources/APIs/LedgerAPI/Repository/LedgerAPI.swift index 45e05d51..662b9843 100644 --- a/Projects/Core/Repository/Sources/APIs/LedgerAPI/Repository/LedgerAPI.swift +++ b/Projects/Core/Repository/Sources/APIs/LedgerAPI/Repository/LedgerAPI.swift @@ -1,6 +1,7 @@ import Foundation import MMNetworkInterface +import Utility import Alamofire diff --git a/Projects/Core/Repository/Sources/APIs/SignAPI/Repository/SignAPI.swift b/Projects/Core/Repository/Sources/APIs/SignAPI/Repository/SignAPI.swift index ee6463e5..320356e4 100644 --- a/Projects/Core/Repository/Sources/APIs/SignAPI/Repository/SignAPI.swift +++ b/Projects/Core/Repository/Sources/APIs/SignAPI/Repository/SignAPI.swift @@ -1,6 +1,7 @@ import Foundation import MMNetworkInterface +import Utility enum SignAPI { case sign(SignRequestDTO) diff --git a/Projects/Core/Repository/Sources/APIs/TokenAPI/Repository/TokenAPI.swift b/Projects/Core/Repository/Sources/APIs/TokenAPI/Repository/TokenAPI.swift index 39492798..63153fcf 100644 --- a/Projects/Core/Repository/Sources/APIs/TokenAPI/Repository/TokenAPI.swift +++ b/Projects/Core/Repository/Sources/APIs/TokenAPI/Repository/TokenAPI.swift @@ -1,6 +1,7 @@ import Foundation import MMNetworkInterface +import Utility import Alamofire diff --git a/Projects/Core/Repository/Sources/APIs/UserAPI/Repository/UserAPI.swift b/Projects/Core/Repository/Sources/APIs/UserAPI/Repository/UserAPI.swift index 9bc0939d..d8ba6f22 100644 --- a/Projects/Core/Repository/Sources/APIs/UserAPI/Repository/UserAPI.swift +++ b/Projects/Core/Repository/Sources/APIs/UserAPI/Repository/UserAPI.swift @@ -1,6 +1,7 @@ import Foundation import MMNetworkInterface +import Utility enum UserAPI { case user // 내정보 조회 diff --git a/Projects/Core/Repository/Sources/APIs/VersionAPI/Repository/VersionAPI.swift b/Projects/Core/Repository/Sources/APIs/VersionAPI/Repository/VersionAPI.swift index 12234835..7c63daa8 100644 --- a/Projects/Core/Repository/Sources/APIs/VersionAPI/Repository/VersionAPI.swift +++ b/Projects/Core/Repository/Sources/APIs/VersionAPI/Repository/VersionAPI.swift @@ -1,6 +1,7 @@ import Foundation import MMNetworkInterface +import Utility enum VersionAPI { case version(VersionRequestDTO) // 버전조회 diff --git a/Projects/Core/Repository/Sources/Common/Config.swift b/Projects/Core/Repository/Sources/Common/Config.swift deleted file mode 100644 index 2f614758..00000000 --- a/Projects/Core/Repository/Sources/Common/Config.swift +++ /dev/null @@ -1,7 +0,0 @@ -enum Config { -#if DEBUG - static let base = "https://dev.moneymong.site/api/" -#else - static let base = "https://prod.moneymong.site/api/" -#endif -} diff --git a/Projects/Domain/Agency/Interface/ConfirmCertificateCodeUseCaseInterface.swift b/Projects/Domain/Agency/Interface/ConfirmCertificateCodeUseCaseInterface.swift index b0f88e59..044c53a7 100644 --- a/Projects/Domain/Agency/Interface/ConfirmCertificateCodeUseCaseInterface.swift +++ b/Projects/Domain/Agency/Interface/ConfirmCertificateCodeUseCaseInterface.swift @@ -5,4 +5,5 @@ import BaseDomain // 소속가입시, 초대코드가 맞는지 확인한다 public protocol ConfirmCertificateCodeUseCaseInterface { func execute(code: [String]) async throws -> Agency? + func execute(code: String, agencyID: Int) async throws -> Agency? } diff --git a/Projects/Domain/Agency/Project.swift b/Projects/Domain/Agency/Project.swift index bae38d91..46392d3d 100644 --- a/Projects/Domain/Agency/Project.swift +++ b/Projects/Domain/Agency/Project.swift @@ -38,7 +38,8 @@ let project = Project( sources: ["Tests/**"], dependencies: [ .target(name: "Agency"), - .target(name: "AgencyTesting") + .target(name: "AgencyTesting"), + .project(target: "BaseDomainTesting", path: .relativeToRoot("Projects/Domain/BaseDomain")), ] ), Target( diff --git a/Projects/Domain/Agency/Sources/ConfirmCertificateCodeUseCase.swift b/Projects/Domain/Agency/Sources/ConfirmCertificateCodeUseCase.swift index 51f1b54d..d476ac3e 100644 --- a/Projects/Domain/Agency/Sources/ConfirmCertificateCodeUseCase.swift +++ b/Projects/Domain/Agency/Sources/ConfirmCertificateCodeUseCase.swift @@ -25,4 +25,21 @@ public struct ConfirmCertificateCodeUseCase: ConfirmCertificateCodeUseCaseInterf } return nil } + + public func execute(code: String, agencyID: Int) async throws -> Agency? { + let agencies = try await agencyRepo.fetchMyAgency() + if let agency = agencies.first(where: { $0.id == agencyID }) { + // 이미 소속에 가입되어 있는 경우 + userRepo.updateSelectedAgency(id: agencyID) + return agency + } + + // 소속에 가입되어 있지 않은 경우 + let response = try await agencyRepo.certificateCode(code: code) + guard response.certified else { return nil } + + userRepo.updateSelectedAgency(id: response.agencyId) + // 방금 가입했으므로 가입 전 조회한 `agencies`(stale)가 아니라 재조회한 목록에서 찾는다. + return try await agencyRepo.fetchMyAgency().first { $0.id == response.agencyId } + } } diff --git a/Projects/Domain/Agency/Testing/MockConfirmCertificateCodeUseCase.swift b/Projects/Domain/Agency/Testing/MockConfirmCertificateCodeUseCase.swift index 6127868e..3de7a003 100644 --- a/Projects/Domain/Agency/Testing/MockConfirmCertificateCodeUseCase.swift +++ b/Projects/Domain/Agency/Testing/MockConfirmCertificateCodeUseCase.swift @@ -5,4 +5,7 @@ public struct MockConfirmCertificateCodeUseCase: ConfirmCertificateCodeUseCaseIn public init() {} public func execute(code: [String]) async throws -> Agency? { return nil } + public func execute(code: String, agencyID: Int) async throws -> Agency? { + return nil + } } diff --git a/Projects/Domain/Agency/Tests/ConfirmCertificateCodeUseCaseTests.swift b/Projects/Domain/Agency/Tests/ConfirmCertificateCodeUseCaseTests.swift new file mode 100644 index 00000000..2e375c98 --- /dev/null +++ b/Projects/Domain/Agency/Tests/ConfirmCertificateCodeUseCaseTests.swift @@ -0,0 +1,120 @@ +import XCTest +@testable import Agency +@testable import BaseDomain +@testable import BaseDomainTesting + +final class ConfirmCertificateCodeUseCaseTests: XCTestCase { + var sut: ConfirmCertificateCodeUseCase! + var mockAgencyRepo: MockAgencyRepository! + var mockUserRepo: MockUserRepository! + + override func setUpWithError() throws { + mockAgencyRepo = MockAgencyRepository() + mockUserRepo = MockUserRepository() + sut = ConfirmCertificateCodeUseCase(agencyRepo: mockAgencyRepo, userRepo: mockUserRepo) + } + + override func tearDownWithError() throws { + mockAgencyRepo = nil + mockUserRepo = nil + sut = nil + } + + func test_execute_코드배열로_호출_시_인증에_성공하면_소속을_저장하고_해당_Agency를_반환한다() async { + // Arrange + let agency = Agency(id: 1, name: "몽테스트", count: 1) + mockAgencyRepo.returnValue.certificateCode = CertificationResult(certified: true, agencyId: agency.id) + mockAgencyRepo.returnValue.fetchMyAgency = [agency] + + do { + // Act + let output = try await sut.execute(code: ["1", "2", "3", "4"]) + + // Assert + XCTAssertEqual(output, agency) + XCTAssertEqual(mockAgencyRepo.inputValue.certificateCode, "1234") + XCTAssertEqual(mockUserRepo.inputValue.updateSelectedAgency, agency.id) + XCTAssertEqual(mockUserRepo.callCount.updateSelectedAgency, 1) + } catch { + // Assert + XCTFail() + } + } + + func test_execute_코드배열로_호출_시_인증에_실패하면_nil을_반환한다() async { + // Arrange + mockAgencyRepo.returnValue.certificateCode = CertificationResult(certified: false, agencyId: 0) + + do { + // Act + let output = try await sut.execute(code: ["1", "2", "3", "4"]) + + // Assert + XCTAssertNil(output) + XCTAssertEqual(mockUserRepo.callCount.updateSelectedAgency, 0) + } catch { + // Assert + XCTFail() + } + } + + func test_execute_코드와_소속ID로_호출_시_이미_가입된_소속이면_인증코드_확인없이_소속을_저장하고_해당_Agency를_반환한다() async { + // Arrange + let agency = Agency(id: 1, name: "몽테스트", count: 1) + mockAgencyRepo.returnValue.fetchMyAgency = [agency] + + do { + // Act + let output = try await sut.execute(code: "1234", agencyID: agency.id) + + // Assert + XCTAssertEqual(output, agency) + XCTAssertEqual(mockAgencyRepo.callCount.certificateCode, 0) + XCTAssertEqual(mockUserRepo.inputValue.updateSelectedAgency, agency.id) + } catch { + // Assert + XCTFail() + } + } + + func test_execute_코드와_소속ID로_호출_시_가입되지_않은_소속이고_인증에_성공하면_재조회한_목록의_Agency를_반환한다() async { + // Arrange + let agency = Agency(id: 2, name: "몽테스트", count: 1) + // 가입 전 조회에는 해당 소속이 없고, 인증(가입) 후 재조회에서야 등장한다. + mockAgencyRepo.returnValue.fetchMyAgencySequence = [[], [agency]] + mockAgencyRepo.returnValue.certificateCode = CertificationResult(certified: true, agencyId: agency.id) + + do { + // Act + let output = try await sut.execute(code: "1234", agencyID: 999) + + // Assert + XCTAssertEqual(output, agency) + XCTAssertEqual(mockAgencyRepo.callCount.certificateCode, 1) + XCTAssertEqual(mockAgencyRepo.callCount.fetchMyAgency, 2) + XCTAssertEqual(mockUserRepo.inputValue.updateSelectedAgency, agency.id) + } catch { + // Assert + XCTFail() + } + } + + func test_execute_코드와_소속ID로_호출_시_가입되지_않은_소속이고_인증에_실패하면_nil을_반환한다() async { + // Arrange + mockAgencyRepo.returnValue.fetchMyAgency = [] + mockAgencyRepo.returnValue.certificateCode = CertificationResult(certified: false, agencyId: 0) + + do { + // Act + let output = try await sut.execute(code: "1234", agencyID: 999) + + // Assert + XCTAssertNil(output) + XCTAssertEqual(mockAgencyRepo.callCount.fetchMyAgency, 1) + XCTAssertEqual(mockUserRepo.callCount.updateSelectedAgency, 0) + } catch { + // Assert + XCTFail() + } + } +} diff --git a/Projects/Domain/BaseDomain/Testing/MockAgencyRepository.swift b/Projects/Domain/BaseDomain/Testing/MockAgencyRepository.swift new file mode 100644 index 00000000..7da814c9 --- /dev/null +++ b/Projects/Domain/BaseDomain/Testing/MockAgencyRepository.swift @@ -0,0 +1,129 @@ +// +// MockAgencyRepository.swift +// BaseDomain +// +// Created by 이시원 on 5/30/26. +// + +import Foundation + +import BaseDomain + +public class MockAgencyRepository: AgencyRepositoryInterface { + public struct CallCount { + public var create = 0 + public var fetchMemberList = 0 + public var changeMemberRole = 0 + public var kickoutMember = 0 + public var fetchMyAgency = 0 + public var fetchCode = 0 + public var certificateCode = 0 + public var reissueCode = 0 + public var deleteAgency = 0 + public var getCategories = 0 + public var createCategory = 0 + public var deleteCategory = 0 + } + + public struct ReturnValue { + public var create: Int? + public var fetchMemberList: [BaseDomain.Member]? + public var fetchMyAgency: [BaseDomain.Agency]? + public var fetchMyAgencySequence: [[BaseDomain.Agency]] = [] + public var fetchCode: String? + public var certificateCode: BaseDomain.CertificationResult? + public var reissueCode: String? + public var getCategories: [BaseDomain.MMCategory]? + } + + public struct InputValue { + public var create: String? = nil + public var fetchMemberList: Int? = nil + public var changeMemberRole: (id: Int, userId: Int, role: String)? = nil + public var kickoutMember: (id: Int, userId: Int)? = nil + public var fetchCode: Int? = nil + public var certificateCode: String? = nil + public var reissueCode: Int? = nil + public var deleteAgency: Int? = nil + public var getCategories: Int? = nil + public var createCategory: (agencyId: Int, name: String)? = nil + public var deleteCategory: Int? = nil + } + + public var callCount: CallCount = .init() + public var returnValue: ReturnValue = .init() + public var inputValue: InputValue = .init() + + public init() {} + + public func create(name: String) async throws -> Int { + callCount.create += 1 + inputValue.create = name + return returnValue.create! + } + + public func fetchMemberList(id: Int) async throws -> [BaseDomain.Member] { + callCount.fetchMemberList += 1 + inputValue.fetchMemberList = id + return returnValue.fetchMemberList! + } + + public func changeMemberRole(id: Int, userId: Int, role: String) async throws { + callCount.changeMemberRole += 1 + inputValue.changeMemberRole = (id: id, userId: userId, role: role) + } + + public func kickoutMember(id: Int, userId: Int) async throws { + callCount.kickoutMember += 1 + inputValue.kickoutMember = (id: id, userId: userId) + } + + public func fetchMyAgency() async throws -> [BaseDomain.Agency] { + callCount.fetchMyAgency += 1 + if !returnValue.fetchMyAgencySequence.isEmpty { + return returnValue.fetchMyAgencySequence.count > 1 + ? returnValue.fetchMyAgencySequence.removeFirst() + : returnValue.fetchMyAgencySequence[0] + } + return returnValue.fetchMyAgency! + } + + public func fetchCode(id: Int) async throws -> String { + callCount.fetchCode += 1 + inputValue.fetchCode = id + return returnValue.fetchCode! + } + + public func certificateCode(code: String) async throws -> BaseDomain.CertificationResult { + callCount.certificateCode += 1 + inputValue.certificateCode = code + return returnValue.certificateCode! + } + + public func reissueCode(id: Int) async throws -> String { + callCount.reissueCode += 1 + inputValue.reissueCode = id + return returnValue.reissueCode! + } + + public func deleteAgency(id: Int) async throws { + callCount.deleteAgency += 1 + inputValue.deleteAgency = id + } + + public func getCategories(id: Int) async throws -> [BaseDomain.MMCategory] { + callCount.getCategories += 1 + inputValue.getCategories = id + return returnValue.getCategories! + } + + public func createCategory(agencyId: Int, name: String) async throws { + callCount.createCategory += 1 + inputValue.createCategory = (agencyId: agencyId, name: name) + } + + public func deleteCategory(id: Int) async throws { + callCount.deleteCategory += 1 + inputValue.deleteCategory = id + } +} diff --git a/Projects/Domain/BaseDomain/Testing/MockUserRepository.swift b/Projects/Domain/BaseDomain/Testing/MockUserRepository.swift new file mode 100644 index 00000000..3dfe4865 --- /dev/null +++ b/Projects/Domain/BaseDomain/Testing/MockUserRepository.swift @@ -0,0 +1,65 @@ +// +// MockUserRepository.swift +// BaseDomain +// +// Created by 이시원 on 5/30/26. +// + +import Foundation + +import BaseDomain + +public class MockUserRepository: UserRepositoryInterface { + public struct CallCount { + public var user = 0 + public var fetchUserID = 0 + public var fetchSelectedAgency = 0 + public var updateSelectedAgency = 0 + public var logout = 0 + public var withdrawl = 0 + } + + public struct ReturnValue { + public var user: BaseDomain.UserInfo? + public var fetchUserID: Int = 0 + public var fetchSelectedAgency: Int? + } + + public struct InputValue { + public var updateSelectedAgency: Int?? = nil + } + + public var callCount: CallCount = .init() + public var returnValue: ReturnValue = .init() + public var inputValue: InputValue = .init() + + public init() {} + + public func user() async throws -> BaseDomain.UserInfo { + callCount.user += 1 + return returnValue.user! + } + + public func fetchUserID() -> Int { + callCount.fetchUserID += 1 + return returnValue.fetchUserID + } + + public func fetchSelectedAgency() -> Int? { + callCount.fetchSelectedAgency += 1 + return returnValue.fetchSelectedAgency + } + + public func updateSelectedAgency(id: Int?) { + callCount.updateSelectedAgency += 1 + inputValue.updateSelectedAgency = id + } + + public func logout() async throws { + callCount.logout += 1 + } + + public func withdrawl() async throws { + callCount.withdrawl += 1 + } +} diff --git a/Projects/Feature/Base/Sources/DeepLinkManager.swift b/Projects/Feature/Base/Sources/DeepLinkManager.swift index f482886a..79050776 100644 --- a/Projects/Feature/Base/Sources/DeepLinkManager.swift +++ b/Projects/Feature/Base/Sources/DeepLinkManager.swift @@ -1,31 +1,25 @@ import Foundation public struct DeepLinkManager { - - public static var destination: [String : Any]? - - // CreateLedger, LedgerDetail - public static func setDestination(_ urlString: String, agencyID: Int?) { - let query = urlString.replacingOccurrences(of: "widget://", with: "") - - guard let agencyID else { return } - - destination = [ - "query": query, - "agencyID": agencyID - ] - + + public static var query: [String : Any]? + public static var notiName: Notification.Name? + + public static func setQuery(_ query: [String : Any], notiName: Notification.Name) { + guard !query.isEmpty else { return } + + self.query = query + self.notiName = notiName + NotificationCenter.default.post( - name: .init("deeplink"), + name: notiName, object: nil, - userInfo: [ - "query": query, - "agencyID": agencyID - ] + userInfo: query ) } - + public static func clear() { - destination = nil + query = nil + notiName = nil } } diff --git a/Projects/Feature/Base/Sources/NSNotificationName+.swift b/Projects/Feature/Base/Sources/NSNotificationName+.swift index d4105e36..02735b8f 100644 --- a/Projects/Feature/Base/Sources/NSNotificationName+.swift +++ b/Projects/Feature/Base/Sources/NSNotificationName+.swift @@ -4,4 +4,6 @@ public extension NSNotification.Name { static let tabBarHidden = NSNotification.Name("tabBarHidden") // toggle Tab static let presentManualCreater = NSNotification.Name("presentManualCreater") // 동아리 운영비 등록화면 present static let presentAgencyCreater = NSNotification.Name("presentAgencyCreater") // 소속 생성화면 present + static let invitationLink = NSNotification.Name("invitationLink") + static let widgetLink = NSNotification.Name("deeplink") } diff --git a/Projects/Feature/Ledger/Sources/Common/LedgerFactory.swift b/Projects/Feature/Ledger/Sources/Common/LedgerFactory.swift index 8e510098..f06b666e 100644 --- a/Projects/Feature/Ledger/Sources/Common/LedgerFactory.swift +++ b/Projects/Feature/Ledger/Sources/Common/LedgerFactory.swift @@ -18,8 +18,8 @@ struct LedgerFactory { let vc = LedgerVC([ledgerTab, memberTab]) vc.reactor = LedgerReactor( getMyAgencyUseCase: DIContainer.shared.resolve(type: GetMyAgencyUseCaseInterface.self), - getSelectedAgency: DIContainer.shared.resolve(type: GetSelectedAgencyUseCaseInterface.self), - updateSelectedAgency: DIContainer.shared.resolve(type: UpdateSelectedAgencyUseCaseInterface.self), + getSelectedAgencyUseCase: DIContainer.shared.resolve(type: GetSelectedAgencyUseCaseInterface.self), + updateSelectedAgencyUseCase: DIContainer.shared.resolve(type: UpdateSelectedAgencyUseCaseInterface.self), ledgerService: DIContainer.shared.resolve(type: LedgerServiceInterface.self) ) return vc @@ -44,7 +44,6 @@ struct LedgerFactory { let vc = MemberTabVC() vc.reactor = MemberTabReactor( getUserIDUseCase: DIContainer.shared.resolve(type: GetUserIDUseCaseInterface.self), - getSelectedAgencyUseCase: DIContainer.shared.resolve(type: GetSelectedAgencyUseCaseInterface.self), reissueCodeUseCase: DIContainer.shared.resolve(type: ReissueCodeUseCaseInterface.self), kickoutMemberUseCase: DIContainer.shared.resolve(type: KickoutMemberUseCaseInterface.self), deleteAgencyUseCase: DIContainer.shared.resolve(type: DeleteAgencyUseCaseInterface.self), diff --git a/Projects/Feature/Ledger/Sources/Scene/Ledger/LedgerReactor.swift b/Projects/Feature/Ledger/Sources/Scene/Ledger/LedgerReactor.swift index 66f9b997..5ddea681 100644 --- a/Projects/Feature/Ledger/Sources/Scene/Ledger/LedgerReactor.swift +++ b/Projects/Feature/Ledger/Sources/Scene/Ledger/LedgerReactor.swift @@ -3,6 +3,7 @@ import Foundation import LedgerFeatureInterface import AgencyInterface import BaseDomain +import BaseFeature import UserInterface import Utility @@ -12,16 +13,19 @@ public final class LedgerReactor: Reactor { public enum Action { case requestMyAgencies + case invite(code: String, agencyID: Int) } public enum Mutation { case setAgency(Agency?) case setError(MoneyMongError) + case setLoading(Bool) } public struct State { @Pulse var agency: Agency? @Pulse var error: MoneyMongError? + @Pulse var isLoading: Bool = false } public let initialState = State() @@ -29,42 +33,73 @@ public final class LedgerReactor: Reactor { private let service: LedgerServiceInterface private let getMyAgencyUseCase: GetMyAgencyUseCaseInterface - private let getSelectedAgency: GetSelectedAgencyUseCaseInterface - private let updateSelectedAgency: UpdateSelectedAgencyUseCaseInterface + private let getSelectedAgencyUseCase: GetSelectedAgencyUseCaseInterface + private let updateSelectedAgencyUseCase: UpdateSelectedAgencyUseCaseInterface + private let confirmCertificateCodeUseCase: ConfirmCertificateCodeUseCaseInterface init( getMyAgencyUseCase: GetMyAgencyUseCaseInterface, - getSelectedAgency: GetSelectedAgencyUseCaseInterface, - updateSelectedAgency: UpdateSelectedAgencyUseCaseInterface, + getSelectedAgencyUseCase: GetSelectedAgencyUseCaseInterface, + updateSelectedAgencyUseCase: UpdateSelectedAgencyUseCaseInterface, + confirmCertificateCodeUseCase: ConfirmCertificateCodeUseCaseInterface = DIContainer.shared.resolve(type: ConfirmCertificateCodeUseCaseInterface.self), ledgerService: LedgerServiceInterface ) { self.getMyAgencyUseCase = getMyAgencyUseCase - self.getSelectedAgency = getSelectedAgency - self.updateSelectedAgency = updateSelectedAgency + self.getSelectedAgencyUseCase = getSelectedAgencyUseCase + self.updateSelectedAgencyUseCase = updateSelectedAgencyUseCase + self.confirmCertificateCodeUseCase = confirmCertificateCodeUseCase self.service = ledgerService } public func mutate(action: Action) -> Observable { switch action { case .requestMyAgencies: - return .task { - try await getMyAgencyUseCase.execute() - } - .map { [weak self] agencies in - let agencyID = self?.getSelectedAgency.execute() - let agency = agencies.first(where: { $0.id == agencyID }) ?? agencies.first - - if let agency { - self?.updateSelectedAgency.execute(id: agency.id) - self?.service.agency.updateAgency(agency) + return .concat( + .just(.setLoading(true)), + requestMyAgenciesMutation(), + .just(.setLoading(false)) + ) + case .invite(let code, let agencyID): + return .concat( + .just(.setLoading(true)), + .task { + try await confirmCertificateCodeUseCase.execute(code: code, agencyID: agencyID) + } + .flatMap { [weak self] agency -> Observable in + guard let self else { return .empty() } + guard let agency else { return requestMyAgenciesMutation() } + applySelected(agency) + return .just(.setAgency(agency)) } + .catch { [weak self] error in + guard let self else { return .just(.setError(error.toMMError)) } + return .concat( + .just(.setError(error.toMMError)), + self.requestMyAgenciesMutation() + ) + }, + .just(.setLoading(false)) + ) + } + } - return .setAgency(agency) + private func requestMyAgenciesMutation() -> Observable { + return .task { try await getMyAgencyUseCase.execute() } + .map { [weak self] agencies -> Agency? in + let selectedID = self?.getSelectedAgencyUseCase.execute() + return agencies.first(where: { $0.id == selectedID }) ?? agencies.first } - .catch { - return .just(.setError($0.toMMError)) - } - } + .do(onNext: { [weak self] agency in + guard let agency else { return } + self?.applySelected(agency) + }) + .map { .setAgency($0) } + .catch { .just(.setError($0.toMMError)) } + } + + private func applySelected(_ agency: Agency) { + updateSelectedAgencyUseCase.execute(id: agency.id) + service.agency.updateAgency(agency) } public func reduce(state: State, mutation: Mutation) -> State { @@ -74,6 +109,8 @@ public final class LedgerReactor: Reactor { newState.agency = agency case let .setError(error): newState.error = error + case .setLoading(let isLoading): + newState.isLoading = isLoading } return newState @@ -87,7 +124,6 @@ public final class LedgerReactor: Reactor { return .just(.setAgency(agency)) } } - return .merge(stream, mutation) } } diff --git a/Projects/Feature/Ledger/Sources/Scene/Ledger/LedgerVC.swift b/Projects/Feature/Ledger/Sources/Scene/Ledger/LedgerVC.swift index 7a56efd9..650ec167 100644 --- a/Projects/Feature/Ledger/Sources/Scene/Ledger/LedgerVC.swift +++ b/Projects/Feature/Ledger/Sources/Scene/Ledger/LedgerVC.swift @@ -58,21 +58,39 @@ public final class LedgerVC: BaseVC, View { public override func setupConstraints() { super.setupConstraints() view.addSubview(emptyView) - + + addChild(lineTab) rootContainer.flex .define { flex in flex.addItem(lineTab.view) } + lineTab.didMove(toParent: self) } public func bind(reactor: LedgerReactor) { setTitle(agencyButton) - rx.viewDidLoad - .map { Reactor.Action.requestMyAgencies } + NotificationCenter.default.rx.notification(.invitationLink) + .compactMap { Self.inviteAction(code: $0.userInfo?["code"], agencyID: $0.userInfo?["agencyID"]) } + .do(onNext: { _ in DeepLinkManager.clear() }) .bind(to: reactor.action) .disposed(by: disposeBag) + rx.viewDidLoad + .map { _ -> Reactor.Action in + if DeepLinkManager.notiName == .invitationLink, + let action = Self.inviteAction( + code: DeepLinkManager.query?["code"], + agencyID: DeepLinkManager.query?["agencyID"] + ) { + DeepLinkManager.clear() + return action + } + return .requestMyAgencies + } + .bind(to: reactor.action) + .disposed(by: disposeBag) + rx.viewDidLoad .bind(with: self, onNext: { owner, _ in owner.coordinator?.moveTab = { owner.lineTab.currentPage = $0 } @@ -85,6 +103,12 @@ public final class LedgerVC: BaseVC, View { } .disposed(by: disposeBag) + agencyButton.rx.tap + .bind(with: self) { owner, _ in + owner.coordinator?.selectAgencySheet() + } + .disposed(by: disposeBag) + reactor.pulse(\.$agency) .skip(1) .observe(on: MainScheduler.instance) @@ -105,10 +129,15 @@ public final class LedgerVC: BaseVC, View { } .disposed(by: disposeBag) - agencyButton.rx.tap - .bind(with: self) { owner, _ in - owner.coordinator?.selectAgencySheet() - } + reactor.pulse(\.$isLoading) + .observe(on: MainScheduler.instance) + .bind(to: rx.isLoading) .disposed(by: disposeBag) } + + private static func inviteAction(code: Any?, agencyID: Any?) -> LedgerReactor.Action? { + guard let code = code as? String, !code.isEmpty, + let agencyID = Int(agencyID as? String ?? "") else { return nil } + return .invite(code: code, agencyID: agencyID) + } } diff --git a/Projects/Feature/Ledger/Sources/Scene/Ledger/Sheets/SelectAgency/SelectAgencySheetVC.swift b/Projects/Feature/Ledger/Sources/Scene/Ledger/Sheets/SelectAgency/SelectAgencySheetVC.swift index a190ee5c..e717bb17 100644 --- a/Projects/Feature/Ledger/Sources/Scene/Ledger/Sheets/SelectAgency/SelectAgencySheetVC.swift +++ b/Projects/Feature/Ledger/Sources/Scene/Ledger/Sheets/SelectAgency/SelectAgencySheetVC.swift @@ -36,13 +36,18 @@ final class SelectAgencySheetVC: BottomSheetVC, View { private let registerCodeInputButton: MMButton = MMButton(title: "초대 코드 입력하기", image: Images.pencil, type: .tertiary) private let createAgencyButton: MMButton = MMButton(title: "새로운 장부 만들기", image: Images.plusCircleLineWhite, type: .primary) + private let loadingIndicator: MMIndicator = .init() override func setupConstraints() { super.setupConstraints() contentView.flex.define { flex in - flex.addItem(tableView).height(3 * (Constant.cellHeight) + Constant.cellSpacing * 3) + flex.addItem(tableView).height(Constant.cellHeight) .marginTop(Constant.topSpacing) .marginHorizontal(Constant.horizontalMargin) + .alignItems(.center) + .define { flex in + flex.addItem(loadingIndicator) + } flex.addItem(registerCodeInputButton) .marginHorizontal(Constant.horizontalMargin) .height(Constant.buttonHeight) @@ -53,7 +58,7 @@ final class SelectAgencySheetVC: BottomSheetVC, View { .marginBottom(Constant.buttonSpacing + Constant.bottomSpacing) componentHeight = Constant.topSpacing + Constant.buttonHeight * 2 + Constant.buttonSpacing * 3 + Constant.bottomSpacing // (버튼 + 마진) 높이 - contentHeight = componentHeight + 3 * (Constant.cellHeight) + Constant.cellSpacing * 2 // 소속 리스트 높이 + contentHeight = componentHeight + (Constant.cellHeight) // 소속 리스트 높이 } } @@ -89,7 +94,12 @@ final class SelectAgencySheetVC: BottomSheetVC, View { .disposed(by: disposeBag) reactor.pulse(\.$isLoading) - .bind(to: rx.isLoading) + .observe(on: MainScheduler.instance) + .bind(with: self) { owner, isLoading in + owner.loadingIndicator.isHidden = !isLoading + owner.loadingIndicator.flex.isIncludedInLayout(isLoading).markDirty() + isLoading ? owner.loadingIndicator.startAnimating() : owner.loadingIndicator.stopAnimating() + } .disposed(by: disposeBag) reactor.pulse(\.$selectedAgencyID) @@ -113,8 +123,8 @@ final class SelectAgencySheetVC: BottomSheetVC, View { .bind(with: self) { owner, agencies in let count = CGFloat(agencies.count) let height = min( - count * 72 + count * 12, - 3 * (72) + 3 * 12 + count * 72 + (count - 1) * 12, + 3 * (72) + 2 * 12 ) owner.tableView.isScrollEnabled = count > 3 diff --git a/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/Components/InvitationLinkButton.swift b/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/Components/InvitationLinkButton.swift new file mode 100644 index 00000000..b9002fe4 --- /dev/null +++ b/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/Components/InvitationLinkButton.swift @@ -0,0 +1,79 @@ +import UIKit + +import DesignSystem + +import PinLayout +import FlexLayout + +final class InvitationLinkButton: UIButton { + private let rootContainer = UIView() + + private let badgeView: UIView = { + let v = UIView() + v.backgroundColor = Colors.Black._1.withAlphaComponent(0.2) + v.layer.cornerRadius = 4 + return v + }() + + private let badgeLabel: UILabel = { + let v = UILabel() + v.font = Fonts.caption + v.textColor = Colors.White._1 + v.text = "이걸로 초대해보세요" + return v + }() + + private let buttonTitleLabel: UILabel = { + let v = UILabel() + v.font = Fonts.body._4 + v.textColor = Colors.White._1 + v.text = "친구 초대하기" + return v + }() + + override var isHighlighted: Bool { + didSet { alpha = isHighlighted ? 0.7 : 1 } + } + + init() { + super.init(frame: .zero) + setupView() + setupConstraints() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + backgroundColor = Colors.Blue._4 + clipsToBounds = true + } + + private func setupConstraints() { + rootContainer.isUserInteractionEnabled = false + addSubview(rootContainer) + + rootContainer.flex + .direction(.row) + .justifyContent(.center) + .alignItems(.center) + .paddingVertical(14) + .define { flex in + flex.addItem(badgeView).marginRight(8).define { flex in + flex.addItem(badgeLabel).marginHorizontal(8).marginVertical(4) + } + flex.addItem(buttonTitleLabel) + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + rootContainer.pin.all() + rootContainer.flex.layout() + + layer.cornerRadius = 10 + } +} diff --git a/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/MemberList/MemberTabReactor.swift b/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/MemberList/MemberTabReactor.swift index 42895615..f70e5fc0 100644 --- a/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/MemberList/MemberTabReactor.swift +++ b/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/MemberList/MemberTabReactor.swift @@ -1,3 +1,5 @@ +import Foundation + import ReactorKit import AgencyInterface @@ -11,7 +13,8 @@ final class MemberTabReactor: Reactor { struct State { let userID: Int - @Pulse var agencyID: Int? + @Pulse var agencyID: Int? = nil + @Pulse var agecnyName: String? @Pulse var name: String? @Pulse var role: Member.Role? @@ -27,6 +30,7 @@ final class MemberTabReactor: Reactor { case kickOffAlert(memberID: Int) case agencyDeleteAlert case ledgerTab + case sharedSheet(content: String) } } @@ -37,11 +41,12 @@ final class MemberTabReactor: Reactor { case tapCodeCopyButton // 초대코드 복사 case tapAgencyDeleteButton // 소속삭제 얼럿 present case tapAgnecyDeleteAlertButton // 소속삭제 얼럿 -> 소속삭제 + case didTapInviteButton } enum Mutation { case setName(String) - case setAgencyID(Int) + case setAgency(id: Int, name: String) case setMembers([Member]) case setRole(Member.Role) case setInvitationCode(String) @@ -54,7 +59,6 @@ final class MemberTabReactor: Reactor { let initialState: State private let getUserIDUseCase: GetUserIDUseCaseInterface - private let getSelectedAgencyUseCase: GetSelectedAgencyUseCaseInterface private let reissueCodeUseCase: ReissueCodeUseCaseInterface private let kickoutMemberUseCase: KickoutMemberUseCaseInterface private let deleteAgencyUseCase: DeleteAgencyUseCaseInterface @@ -66,7 +70,6 @@ final class MemberTabReactor: Reactor { init( getUserIDUseCase: GetUserIDUseCaseInterface, - getSelectedAgencyUseCase: GetSelectedAgencyUseCaseInterface, reissueCodeUseCase: ReissueCodeUseCaseInterface, kickoutMemberUseCase: KickoutMemberUseCaseInterface, deleteAgencyUseCase: DeleteAgencyUseCaseInterface, @@ -76,7 +79,6 @@ final class MemberTabReactor: Reactor { ledgerService: LedgerServiceInterface ) { self.getUserIDUseCase = getUserIDUseCase - self.getSelectedAgencyUseCase = getSelectedAgencyUseCase self.reissueCodeUseCase = reissueCodeUseCase self.kickoutMemberUseCase = kickoutMemberUseCase self.deleteAgencyUseCase = deleteAgencyUseCase @@ -87,7 +89,6 @@ final class MemberTabReactor: Reactor { self.initialState = .init( userID: getUserIDUseCase.execute(), - agencyID: getSelectedAgencyUseCase.execute() ) } @@ -154,6 +155,9 @@ final class MemberTabReactor: Reactor { return .setDestination(.ledgerTab) } .catch { return .just(.setError($0.toMMError)) } + case .didTapInviteButton: + guard let content = makeInvitationContent() else { return .empty() } + return .just(.setDestination(.sharedSheet(content: content))) } } @@ -163,8 +167,9 @@ final class MemberTabReactor: Reactor { case let .setName(name): newState.name = name - case let .setAgencyID(id): + case let .setAgency(id, name): newState.agencyID = id + newState.agecnyName = name case let .setInvitationCode(code): newState.invitationCode = code @@ -219,9 +224,9 @@ final class MemberTabReactor: Reactor { case let .update(agency): if currentState.agencyID == agency?.id { return .empty() - } else if let id = agency?.id { + } else if let id = agency?.id, let name = agency?.name { return .concat( - .just(.setAgencyID(id)), + .just(.setAgency(id: id, name: name)), .just(.setLoading(true)), requestInvitationCode(agencyID: id), requestMembers(agencyID: id), @@ -274,4 +279,15 @@ final class MemberTabReactor: Reactor { ) } } + + private func makeInvitationContent() -> String? { + guard let code = currentState.invitationCode, + let agencyID = currentState.agencyID, + let invitationURL = Config.invitationURL(code: code, agencyID: agencyID), + let userName = currentState.name, + let angecyName = currentState.agecnyName else { return nil } + + return "\(userName)님이 \(angecyName) 장부에 초대했어요.\n초대 받고 함께 쓴 돈을 같이 관리해봐요!\n\n\(invitationURL)" + + } } diff --git a/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/MemberList/MemberTabVC.swift b/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/MemberList/MemberTabVC.swift index 0383d0f4..5b6cb4cd 100644 --- a/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/MemberList/MemberTabVC.swift +++ b/Projects/Feature/Ledger/Sources/Scene/Ledger/TabItems/Member/MemberList/MemberTabVC.swift @@ -21,6 +21,7 @@ final class MemberTabVC: BaseVC, View { }() private let profileView = MyProfileView() + private let invitationLinkButton = InvitationLinkButton() private let emptyView = MemberEmptyView() private let memberHeaderLabel: UILabel = { @@ -49,7 +50,8 @@ final class MemberTabVC: BaseVC, View { rootContainer.flex.paddingHorizontal(20).define { flex in flex.addItem(profileHeaderLabel).marginTop(24).marginBottom(8) - flex.addItem(profileView).marginBottom(24) + flex.addItem(profileView).marginBottom(16) + flex.addItem(invitationLinkButton).marginBottom(20) flex.addItem(memberHeaderLabel).marginBottom(8) flex.addItem(tableView).grow(1) } @@ -82,6 +84,11 @@ final class MemberTabVC: BaseVC, View { .map { Reactor.Action.tapAgencyDeleteButton } .bind(to: reactor.action) .disposed(by: disposeBag) + + invitationLinkButton.rx.tap + .map { Reactor.Action.didTapInviteButton } + .bind(to: reactor.action) + .disposed(by: disposeBag) tableView.rx.modelSelected(Member.self) .bind(with: self) { owner, member in @@ -120,6 +127,9 @@ final class MemberTabVC: BaseVC, View { .observe(on: MainScheduler.instance) .bind(with: self) { owner, element in owner.profileView.configure(title: element.name, role: element.role, code: element.code) + owner.invitationLinkButton.isHidden = element.role == .member + owner.invitationLinkButton.flex.isIncludedInLayout(element.role != .member).markDirty() + owner.profileView.flex.marginBottom(element.role != .member ? 16 : 24).markDirty() } .disposed(by: disposeBag) @@ -158,8 +168,20 @@ final class MemberTabVC: BaseVC, View { reactor.action.onNext(.tapAgnecyDeleteAlertButton) }, cancelAction: {}) )) + case let .sharedSheet(content): + owner.showShareSheet(content: content) } } .disposed(by: disposeBag) } } + +private extension MemberTabVC { + func showShareSheet(content: String) { + let activityVC = UIActivityViewController( + activityItems: [content], + applicationActivities: nil + ) + present(activityVC, animated: true) + } +} diff --git a/Projects/Feature/Main/Sources/MainTapViewController.swift b/Projects/Feature/Main/Sources/MainTapViewController.swift index 1ae9eb50..07237d98 100644 --- a/Projects/Feature/Main/Sources/MainTapViewController.swift +++ b/Projects/Feature/Main/Sources/MainTapViewController.swift @@ -59,14 +59,14 @@ public final class MainTapViewController: UITabBarController { private func observeNotification() { NotificationCenter.default.rx - .notification(.init("deeplink")) - .compactMap { noti -> (query: String, agencyID: Int)? in - guard let query = noti.userInfo?["query"] as? String, + .notification(.widgetLink) + .compactMap { noti -> (destination: String, agencyID: Int)? in + guard let destination = noti.userInfo?["destination"] as? String, let agencyID = noti.userInfo?["agencyID"] as? Int else { return nil } - return (query, agencyID) + return (destination, agencyID) } .bind(with: self) { owner, userInfo in - switch userInfo.query { + switch userInfo.destination { case "CreateLedger": owner.coordinator?.move(to: .createManualLedger(userInfo.agencyID)) case "LedgerDetail": @@ -80,8 +80,8 @@ public final class MainTapViewController: UITabBarController { } .disposed(by: disposeBag) - if let destination = DeepLinkManager.destination { - NotificationCenter.default.post(name: .init("deeplink"), object: nil, userInfo: destination) + if let destination = DeepLinkManager.query, DeepLinkManager.notiName == .widgetLink { + NotificationCenter.default.post(name: .widgetLink, object: nil, userInfo: destination) } } } diff --git a/Projects/Feature/Sign/Sources/Scene/Login/LoginReactor.swift b/Projects/Feature/Sign/Sources/Scene/Login/LoginReactor.swift index 69592f49..e4742bd9 100644 --- a/Projects/Feature/Sign/Sources/Scene/Login/LoginReactor.swift +++ b/Projects/Feature/Sign/Sources/Scene/Login/LoginReactor.swift @@ -57,6 +57,7 @@ final class LoginReactor: Reactor { case .login(let loginType): return .task { _ = try await signUpUseCase.execute(loginType: loginType) + if DeepLinkManager.notiName == .invitationLink { return true } return try await !getMyAgencyUseCase.execute().isEmpty } .map { .setDestination($0 ? .main : .signUp) } diff --git a/Projects/Shared/DesignSystem/Sources/Tabs/LineTabViewController.swift b/Projects/Shared/DesignSystem/Sources/Tabs/LineTabViewController.swift index b1baa01c..f4bc3380 100644 --- a/Projects/Shared/DesignSystem/Sources/Tabs/LineTabViewController.swift +++ b/Projects/Shared/DesignSystem/Sources/Tabs/LineTabViewController.swift @@ -79,10 +79,11 @@ public final class LineTabViewController: UIViewController { private func setupView() {} private func setupConstraints() { + addChild(pageViewController) view.addSubview(rootContainer) rootContainer.addSubview(pageViewController.view) rootContainer.addSubview(tabView) - + pageViewController.didMove(toParent: self) } @objc diff --git a/Projects/Shared/Utility/Sources/Config.swift b/Projects/Shared/Utility/Sources/Config.swift new file mode 100644 index 00000000..44370fc8 --- /dev/null +++ b/Projects/Shared/Utility/Sources/Config.swift @@ -0,0 +1,22 @@ +import Foundation + +public enum Config { +#if DEBUG + public static let base = "https://dev.moneymong.site/api/" + public static let webBase = "https://dev.moneymong.site" +#else + public static let base = "https://prod.moneymong.site/api/" + public static let webBase = "https://prod.moneymong.site" +#endif + + public static let invitationPath = "/invite" + + public static func invitationURL(code: String, agencyID: Int) -> String? { + var components = URLComponents(string: webBase + invitationPath) + components?.queryItems = [ + URLQueryItem(name: "code", value: code), + URLQueryItem(name: "agencyID", value: String(agencyID)) + ] + return components?.url?.absoluteString + } +}