diff --git a/BeforeGoing.xcodeproj/project.pbxproj b/BeforeGoing.xcodeproj/project.pbxproj index 200b2056..ae56052b 100644 --- a/BeforeGoing.xcodeproj/project.pbxproj +++ b/BeforeGoing.xcodeproj/project.pbxproj @@ -267,12 +267,14 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BeforeGoing/Resource/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "\"현재 위치 사용을 허용하시겠습니까?\""; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"앱을 사용하는 동안 위치 사용을 허용하시겠습니까?\""; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "앱 사용 여부와 관계없이 현재 위치의 날씨를 확인하고 정확한 알림을 제공하는 데 사용됩니다."; + INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "앱이 닫혀 있을 때에도 날씨 정보를 미리 업데이트하여 정확한 정보를 제공하기 위해 위치를 사용합니다."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "현재 위치의 날씨 정보를 제공하고, 사용자가 직접 미션을 추가할 때 지리적 태그를 지정하기 위해 필요합니다."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -307,12 +309,14 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BeforeGoing/Resource/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle"; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "\"현재 위치 사용을 허용하시겠습니까?\""; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"앱을 사용하는 동안 위치 사용을 허용하시겠습니까?\""; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "앱 사용 여부와 관계없이 현재 위치의 날씨를 확인하고 정확한 알림을 제공하는 데 사용됩니다."; + INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "앱이 닫혀 있을 때에도 날씨 정보를 미리 업데이트하여 정확한 정보를 제공하기 위해 위치를 사용합니다."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "현재 위치의 날씨 정보를 제공하고, 사용자가 직접 미션을 추가할 때 지리적 태그를 지정하기 위해 필요합니다."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/BeforeGoing/Application/AppDelegate.swift b/BeforeGoing/Application/AppDelegate.swift index c4b0ffb1..f6ec062c 100644 --- a/BeforeGoing/Application/AppDelegate.swift +++ b/BeforeGoing/Application/AppDelegate.swift @@ -16,12 +16,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { DIContainer.shared.injectDependency() + initKakaoSDK() - if let appKey = Bundle.main.object(forInfoDictionaryKey: "KAKAO_NATIVE_APP_KEY") as? String { - KakaoSDK.initSDK(appKey: appKey) - } else { - fatalError("카카오 네이티브 앱 키 없음") - } return true } @@ -49,3 +45,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationWillTerminate(_ application: UIApplication) {} } +extension AppDelegate { + + func initKakaoSDK() { + if let appKey = Bundle.main.object(forInfoDictionaryKey: "KAKAO_NATIVE_APP_KEY") as? String { + KakaoSDK.initSDK(appKey: appKey) + } else { + fatalError("카카오 네이티브 앱 키 없음") + } + } +} diff --git a/BeforeGoing/Core/BeforeGoingError.swift b/BeforeGoing/Core/BeforeGoingError.swift index ca034553..11a3670f 100644 --- a/BeforeGoing/Core/BeforeGoingError.swift +++ b/BeforeGoing/Core/BeforeGoingError.swift @@ -29,4 +29,5 @@ enum BeforeGoingError: Error, Equatable { case missionLimitError case tooManyRequset case withdrawFailed + case notFoundProvider } diff --git a/BeforeGoing/Data/Model/Auth/Response/LoginResponseDTO.swift b/BeforeGoing/Data/Model/Auth/Response/LoginResponseDTO.swift index 4cecfa49..5b384678 100644 --- a/BeforeGoing/Data/Model/Auth/Response/LoginResponseDTO.swift +++ b/BeforeGoing/Data/Model/Auth/Response/LoginResponseDTO.swift @@ -4,6 +4,7 @@ struct LoginResponseDTO: Decodable { let accessTokenExpiresIn: Int let refreshToken: String let refreshTokenExpiresIn: Int + let isNewMember: Bool } extension LoginResponseDTO { @@ -13,7 +14,8 @@ extension LoginResponseDTO { accessToken: accessToken, accessTokenExpiresIn: accessTokenExpiresIn, refreshToken: refreshToken, - refreshTokenExpiresIn: refreshTokenExpiresIn + refreshTokenExpiresIn: refreshTokenExpiresIn, + isNewMember: isNewMember ) } } diff --git a/BeforeGoing/Data/Network/Service/API/MemberAPI.swift b/BeforeGoing/Data/Network/Service/API/MemberAPI.swift index d7dc17a7..bb307341 100644 --- a/BeforeGoing/Data/Network/Service/API/MemberAPI.swift +++ b/BeforeGoing/Data/Network/Service/API/MemberAPI.swift @@ -10,6 +10,7 @@ import Alamofire enum MemberAPI { case updateNickname(accessToken: String, dto: UpdateNicknameRequestDTO) case withdraw(accessToken: String) + case fetchMemberName(accessToken: String) } extension MemberAPI: EndPoint { @@ -22,7 +23,7 @@ extension MemberAPI: EndPoint { let basePath = Environment.baseURL + basePath switch self { - case .updateNickname: + case .updateNickname, .fetchMemberName: return basePath + "/nickname" case .withdraw: return basePath @@ -35,6 +36,8 @@ extension MemberAPI: EndPoint { return .patch case .withdraw: return .delete + case .fetchMemberName: + return .get } } @@ -44,7 +47,7 @@ extension MemberAPI: EndPoint { var headers: HTTPHeaders? { switch self { - case .updateNickname(let accessToken, _), .withdraw(let accessToken): + case .updateNickname(let accessToken, _), .withdraw(let accessToken), .fetchMemberName(let accessToken): return [ "Content-Type": "application/json", "Authorization": "Bearer \(accessToken)" @@ -54,7 +57,7 @@ extension MemberAPI: EndPoint { var parameterEncoding: any ParameterEncoding { switch self { - case .updateNickname: + case .updateNickname, .fetchMemberName: return JSONEncoding.default case .withdraw: return URLEncoding.default @@ -69,7 +72,7 @@ extension MemberAPI: EndPoint { switch self { case .updateNickname(_, let dto): return try? dto.toBodyParameters() - case .withdraw: + case .withdraw, .fetchMemberName: return nil } } diff --git a/BeforeGoing/Data/Network/Service/NetworkService.swift b/BeforeGoing/Data/Network/Service/NetworkService.swift index 7fec1d12..1a57d387 100644 --- a/BeforeGoing/Data/Network/Service/NetworkService.swift +++ b/BeforeGoing/Data/Network/Service/NetworkService.swift @@ -10,6 +10,7 @@ protocol APIManaging { responseType: T.Type ) async throws -> T func request(endPoint: any EndPoint) async throws + func requestString(endPoint: EndPoint) async throws -> String func requestKakaoIDToken(nonce: String?) async throws -> String } @@ -45,6 +46,23 @@ final class NetworkService: APIManaging { let response = try await dataRequest.serializingData().value writeLog(response: response) } catch { + if let afError = error.asAFError { + throw handleError(afError: afError) + } + throw error + } + } + + func requestString(endPoint: EndPoint) async throws -> String { + do { + let dataRequest = createDataRequest(endPoint: endPoint) + let response = try await dataRequest.serializingString().value + writeLog(response: response) + return response + } catch { + if let afError = error.asAFError { + throw handleError(afError: afError) + } throw error } } @@ -73,28 +91,24 @@ final class NetworkService: APIManaging { } } - switch afError { - case .responseValidationFailed(let reason): - if case .unacceptableStatusCode(let statuscode) = reason { - switch statuscode { - case 304: - return .notModifiedError - case 400: - return .badRequestError - case 404: - return .notFoundError - case 429: - return .tooManyRequset - case (500...599): - return .serviceUnavailable - default: - return .unknownError - } + if let statusCode = afError.responseCode { + switch statusCode { + case 304: + return .notModifiedError + case 400: + return .badRequestError + case 404: + return .notFoundError + case 429: + return .tooManyRequset + case 500...599: + return .serviceUnavailable + default: + break } - return .unknownError - default: - return .unknownError } + + return .unknownError } } diff --git a/BeforeGoing/Data/Persistence/Service/UserDefaultsKey.swift b/BeforeGoing/Data/Persistence/Service/UserDefaultsKey.swift index 81e1b8c8..5d25d9a7 100644 --- a/BeforeGoing/Data/Persistence/Service/UserDefaultsKey.swift +++ b/BeforeGoing/Data/Persistence/Service/UserDefaultsKey.swift @@ -6,12 +6,6 @@ // enum UserDefaultsKey: String, CaseIterable { - case isKakaoCompletedAgreeTerms - case isAppleCompletedAgreeTerms - case isKakaoCompletedOnboarding - case isAppleCompletedOnboarding - case kakaoMemberName - case appleMemberName case provider case lastProvider } diff --git a/BeforeGoing/Data/Repository/AuthRepository.swift b/BeforeGoing/Data/Repository/AuthRepository.swift index 413dcbd0..5a00cd37 100644 --- a/BeforeGoing/Data/Repository/AuthRepository.swift +++ b/BeforeGoing/Data/Repository/AuthRepository.swift @@ -51,7 +51,7 @@ struct AuthRepository: AuthInterface { return try await requestLogin(provider: provider, idToken: idToken) } - func requestLogin(provider: Provider, idToken: String) async throws -> Bool { + private func requestLogin(provider: Provider, idToken: String) async throws -> Bool { let requestDTO = loginRequestMapper.map((provider.rawValue, idToken)) let response = try await networkService.request( endPoint: AuthAPI.login(dto: requestDTO), @@ -60,16 +60,35 @@ struct AuthRepository: AuthInterface { saveKeyChain(response: response) saveProvider(provider) - return isCompletedOnboarding(provider: provider) + let isAgreedTerms = try await isAgreedTerms(accessToken: response.accessToken) + let isCompletedJoin = !response.isNewMember && isAgreedTerms + return isCompletedJoin + } + + func requestLogin(provider: Provider, idToken: String, name: String?) async throws -> Bool { + let isCompletedJoin = try await requestLogin(provider: provider, idToken: idToken) + + if let name, + !name.isBlank, + let accessToken = keyChainService.load(key: .accessToken) { + try await networkService.request( + endPoint: MemberAPI.updateNickname( + accessToken: accessToken, + dto: .init(nickname: name) + ) + ) + } + + return isCompletedJoin } func autoLogin() async throws -> Bool { - guard let providerString: String = userDefaultsService.load(key: .provider), - let provider = Provider(rawValue: providerString) else { + guard let accessToken = keyChainService.load(key: .accessToken) else { return false } - guard isTokenExists, isCompletedOnboarding(provider: provider) else { + guard isTokenExists, + try await isAgreedTerms(accessToken: accessToken) else { return false } @@ -148,13 +167,16 @@ struct AuthRepository: AuthInterface { return false } - private func isCompletedOnboarding(provider: Provider) -> Bool { - let key: UserDefaultsKey = (provider == .apple) ? .isAppleCompletedOnboarding : .isKakaoCompletedOnboarding - - guard let isCompleted: Bool = userDefaultsService.load(key: key) else { + private func isAgreedTerms(accessToken: String) async throws -> Bool { + do { + let _ = try await networkService.request( + endPoint: TermsAPI.getTerms(accessToken: accessToken), + responseType: TermsResponseDTO.self + ) + return true + } catch { return false } - return isCompleted } private func deleteUserInformation() { diff --git a/BeforeGoing/Data/Repository/MemberRepository.swift b/BeforeGoing/Data/Repository/MemberRepository.swift index d2c06c8d..21446d85 100644 --- a/BeforeGoing/Data/Repository/MemberRepository.swift +++ b/BeforeGoing/Data/Repository/MemberRepository.swift @@ -24,18 +24,23 @@ struct MemberRepository: MemberInterface { self.updateNicknameRequestMapper = updateNicknameRequestMapper } - func getMemberName() -> String? { + var isAppleLogined: Bool? { guard let provider: String = userDefaultsService.load(key: .provider) else { return nil } - switch provider { - case Provider.kakao.rawValue: - return userDefaultsService.load(key: .kakaoMemberName) - case Provider.apple.rawValue: - return userDefaultsService.load(key: .appleMemberName) - default: - return nil + if provider == Provider.apple.rawValue { + return true + } + return false + } + + func getMemberName() async throws -> MemberNameEntity { + do { + let memberName = try await fetchMemberName() + return memberName + } catch { + return .stub() } } @@ -44,27 +49,17 @@ struct MemberRepository: MemberInterface { BeforeGoingLogger.error(BeforeGoingError.accessTokenMissing) return } - guard let provider: String = userDefaultsService.load(key: .provider) else { - return - } let requestDTO = updateNicknameRequestMapper.map(nickname) - let responseDTO = try await networkService.request( + let _ = try await networkService.request( endPoint: MemberAPI.updateNickname(accessToken: accessToken, dto: requestDTO), responseType: MemberResponseDTO.self ) - - if provider == Provider.apple.rawValue { - let _ = userDefaultsService.save(responseDTO.nickname, key: .appleMemberName) - } - if provider == Provider.kakao.rawValue { - let _ = userDefaultsService.save(responseDTO.nickname, key: .kakaoMemberName) - } } func withdrawMember() async throws { guard let accessToken = keyChainService.load(key: .accessToken), - let provider: String = userDefaultsService.load(key: .provider) else { + let provider: String = userDefaultsService.load(key: .provider) else { BeforeGoingLogger.error(BeforeGoingError.accessTokenMissing) return } @@ -78,15 +73,17 @@ struct MemberRepository: MemberInterface { } } - func completeOnboarding() -> Bool { - guard let providerString: String = userDefaultsService.load(key: .provider), - let provider = Provider(rawValue: providerString) else { - return false + private func fetchMemberName() async throws -> MemberNameEntity { + guard let accessToken = keyChainService.load(key: .accessToken) else { + BeforeGoingLogger.error(BeforeGoingError.accessTokenMissing) + return .stub() } -let key: UserDefaultsKey = (provider == .apple) ? .isAppleCompletedOnboarding : .isKakaoCompletedOnboarding - let isSaved = userDefaultsService.save(true, key: key) - return isSaved + let fetchedName = try await networkService.requestString( + endPoint: MemberAPI.fetchMemberName(accessToken: accessToken) + ) + + return .init(memberName: fetchedName) } private func removeMemberInfo(provider: String) { @@ -105,24 +102,9 @@ let key: UserDefaultsKey = (provider == .apple) ? .isAppleCompletedOnboarding : } private func removeUserDefaultsInfo(provider: String) { - let excludedKeys: [UserDefaultsKey?] = { - switch provider { - case Provider.kakao.rawValue: return [ - .appleMemberName, - .isAppleCompletedOnboarding, - .isAppleCompletedAgreeTerms - ] - case Provider.apple.rawValue: return [ - .kakaoMemberName, - .isKakaoCompletedOnboarding, - .isKakaoCompletedAgreeTerms - ] -default: return [] - } - }() - UserDefaultsKey.allCases - .filter { !excludedKeys.contains($0) } - .forEach { let _ = userDefaultsService.delete(key: $0) } + .forEach { + let _ = userDefaultsService.delete(key: $0) + } } } diff --git a/BeforeGoing/Data/Repository/TermsRepository.swift b/BeforeGoing/Data/Repository/TermsRepository.swift index 8890e48e..3c9435e1 100644 --- a/BeforeGoing/Data/Repository/TermsRepository.swift +++ b/BeforeGoing/Data/Repository/TermsRepository.swift @@ -51,14 +51,8 @@ struct TermsRepository: TermsInterface { return } - guard let providerString: String = userDefaultsService.load(key: .provider) else { - return - } - - let provider = Provider(rawValue: providerString) - let key: UserDefaultsKey = (provider == .apple) ? .isAppleCompletedAgreeTerms : .isKakaoCompletedAgreeTerms - - if let _: Bool = userDefaultsService.load(key: key) { + let isAgreedTerms = try await isAgreedTerms(accessToken: accessToken) + if isAgreedTerms { try await updateAgreementTerm(eventPushAgreed: eventPushAgreed) return } @@ -75,8 +69,6 @@ struct TermsRepository: TermsInterface { endPoint: TermsAPI.sendTerms(accessToken: accessToken, dto: requestDTO), responseType: TermsResponseDTO.self ) - - let _ = (provider == .apple) ? userDefaultsService.save(true, key: .isAppleCompletedAgreeTerms) : userDefaultsService.save(true, key: .isKakaoCompletedAgreeTerms) } func updateAgreementTerm(eventPushAgreed: Bool) async throws { @@ -91,4 +83,13 @@ struct TermsRepository: TermsInterface { responseType: TermsResponseDTO.self ) } + + private func isAgreedTerms(accessToken: String) async throws -> Bool { + do { + let _ = try await getAgreementTerms() + return true + } catch { + return false + } + } } diff --git a/BeforeGoing/Domain/DomainDependencyAssembler.swift b/BeforeGoing/Domain/DomainDependencyAssembler.swift index 1223cca8..a0a8c174 100644 --- a/BeforeGoing/Domain/DomainDependencyAssembler.swift +++ b/BeforeGoing/Domain/DomainDependencyAssembler.swift @@ -94,6 +94,9 @@ final class DomainDependencyAssembler: DependencyAssembler { DIContainer.shared.register(type: SendAgreeTermsType.self) { _ in return SendAgreeTermsUseCase(repository: termsRepository) } + DIContainer.shared.register(type: IsAppleLoginType.self) { _ in + return IsAppleLoginedUseCase(repository: memberRepository) + } DIContainer.shared.register(type: UpdatePushNoticeType.self) { _ in return UpdatePushNoticeUseCase(repository: termsRepository) } @@ -107,9 +110,6 @@ final class DomainDependencyAssembler: DependencyAssembler { DIContainer.shared.register(type: MemberWithdrawType.self) { _ in return MemberWithdrawUseCase(repository: memberRepository) } - DIContainer.shared.register(type: SaveOnboardingCompletedType.self) { _ in - return SaveOnboardingCompletedUseCase(repository: memberRepository) - } DIContainer.shared.register(type: FetchWeatherType.self) { _ in return FetchWeatherUseCase() diff --git a/BeforeGoing/Domain/Entity/LoginEntity.swift b/BeforeGoing/Domain/Entity/LoginEntity.swift index 02350b92..5c0cc153 100644 --- a/BeforeGoing/Domain/Entity/LoginEntity.swift +++ b/BeforeGoing/Domain/Entity/LoginEntity.swift @@ -11,4 +11,5 @@ struct LoginEntity { let accessTokenExpiresIn: Int let refreshToken: String let refreshTokenExpiresIn: Int + let isNewMember: Bool } diff --git a/BeforeGoing/Domain/Entity/MemberNameEntity.swift b/BeforeGoing/Domain/Entity/MemberNameEntity.swift new file mode 100644 index 00000000..57a354b7 --- /dev/null +++ b/BeforeGoing/Domain/Entity/MemberNameEntity.swift @@ -0,0 +1,16 @@ +// +// MemberNameEntity.swift +// BeforeGoing +// +// Created by APPLE on 1/12/26. +// + +struct MemberNameEntity { + let memberName: String +} + +extension MemberNameEntity { + static func stub() -> Self { + return .init(memberName: "워리") + } +} diff --git a/BeforeGoing/Domain/Interface/AuthInterface.swift b/BeforeGoing/Domain/Interface/AuthInterface.swift index 6f2c5b6a..cf6f128c 100644 --- a/BeforeGoing/Domain/Interface/AuthInterface.swift +++ b/BeforeGoing/Domain/Interface/AuthInterface.swift @@ -9,7 +9,7 @@ protocol AuthInterface { func requestNonce(provider: Provider) async throws -> NonceEntity func requestLogin(provider: Provider) async throws -> Bool - func requestLogin(provider: Provider, idToken: String) async throws -> Bool + func requestLogin(provider: Provider, idToken: String, name: String?) async throws -> Bool func autoLogin() async throws -> Bool func getLastLogin() -> Provider? func logout() async throws diff --git a/BeforeGoing/Domain/Interface/MemberInterface.swift b/BeforeGoing/Domain/Interface/MemberInterface.swift index 9de2929b..188de120 100644 --- a/BeforeGoing/Domain/Interface/MemberInterface.swift +++ b/BeforeGoing/Domain/Interface/MemberInterface.swift @@ -7,8 +7,9 @@ protocol MemberInterface { + var isAppleLogined: Bool? { get } + func updateNickname(nickname: String) async throws func withdrawMember() async throws - func getMemberName() -> String? - func completeOnboarding() -> Bool + func getMemberName() async throws -> MemberNameEntity } diff --git a/BeforeGoing/Domain/UseCase/Auth/LoginUseCase.swift b/BeforeGoing/Domain/UseCase/Auth/LoginUseCase.swift index 028ca6b7..a6d6dbf2 100644 --- a/BeforeGoing/Domain/UseCase/Auth/LoginUseCase.swift +++ b/BeforeGoing/Domain/UseCase/Auth/LoginUseCase.swift @@ -8,7 +8,7 @@ protocol LoginType { func login(provider: Provider) async throws -> Bool - func login(provider: Provider, idToken: String) async throws -> Bool + func login(provider: Provider, idToken: String, name: String?) async throws -> Bool func requestNonce(provider: Provider) async throws -> String } @@ -24,8 +24,12 @@ struct LoginUseCase: LoginType { try await repository.requestLogin(provider: provider) } - func login(provider: Provider, idToken: String) async throws -> Bool { - return try await repository.requestLogin(provider: provider, idToken: idToken) + func login(provider: Provider, idToken: String, name: String?) async throws -> Bool { + return try await repository.requestLogin( + provider: provider, + idToken: idToken, + name: name + ) } func requestNonce(provider: Provider) async throws -> String { @@ -39,7 +43,7 @@ struct MockLoginUseCase: LoginType { return true } - func login(provider: Provider, idToken: String) -> Bool { + func login(provider: Provider, idToken: String, name: String?) -> Bool { return true } diff --git a/BeforeGoing/Domain/UseCase/Member/GetMemberNameUseCase.swift b/BeforeGoing/Domain/UseCase/Member/GetMemberNameUseCase.swift index 014ca07e..f8f30fb4 100644 --- a/BeforeGoing/Domain/UseCase/Member/GetMemberNameUseCase.swift +++ b/BeforeGoing/Domain/UseCase/Member/GetMemberNameUseCase.swift @@ -6,7 +6,7 @@ // protocol GetMemberNameType { - func execute() -> String + func execute() async throws -> String } struct GetMemberNameUseCase: GetMemberNameType { @@ -17,9 +17,9 @@ struct GetMemberNameUseCase: GetMemberNameType { self.repository = repository } - func execute() -> String { - let name = repository.getMemberName() ?? "" - return name + func execute() async throws -> String { + let result = try await repository.getMemberName() + return result.memberName } } diff --git a/BeforeGoing/Domain/UseCase/Member/IsAppleLoginedUseCase.swift b/BeforeGoing/Domain/UseCase/Member/IsAppleLoginedUseCase.swift new file mode 100644 index 00000000..511be4b5 --- /dev/null +++ b/BeforeGoing/Domain/UseCase/Member/IsAppleLoginedUseCase.swift @@ -0,0 +1,24 @@ +// +// IsAppleLoginedUseCase.swift +// BeforeGoing +// +// Created by APPLE on 1/6/26. +// + +protocol IsAppleLoginType { + + func execute() -> Bool? +} + +struct IsAppleLoginedUseCase: IsAppleLoginType { + + private let repository: MemberInterface + + init(repository: MemberInterface) { + self.repository = repository + } + + func execute() -> Bool? { + repository.isAppleLogined + } +} diff --git a/BeforeGoing/Domain/UseCase/Member/SaveOnboardingCompletedUseCase.swift b/BeforeGoing/Domain/UseCase/Member/SaveOnboardingCompletedUseCase.swift deleted file mode 100644 index 5aa2bcc3..00000000 --- a/BeforeGoing/Domain/UseCase/Member/SaveOnboardingCompletedUseCase.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// SaveIsCompletedOnboardingUseCase.swift -// BeforeGoing -// -// Created by APPLE on 11/13/25. -// - -protocol SaveOnboardingCompletedType { - func saveOnboardingCompleted() -> Bool -} - -struct SaveOnboardingCompletedUseCase: SaveOnboardingCompletedType { - - private let repository: MemberInterface - - init(repository: MemberInterface) { - self.repository = repository - } - - func saveOnboardingCompleted() -> Bool { - repository.completeOnboarding() - } -} diff --git a/BeforeGoing/Presentation/Common/ScenarioEmptyView.swift b/BeforeGoing/Presentation/Common/ScenarioEmptyView.swift index c0468437..eee513bf 100644 --- a/BeforeGoing/Presentation/Common/ScenarioEmptyView.swift +++ b/BeforeGoing/Presentation/Common/ScenarioEmptyView.swift @@ -18,6 +18,9 @@ final class ScenarioEmptyView: BaseView { state: .addScenarioButton, title: "+ 시나리오 추가" ) + private(set) var weatherKitButtonView = WeatherKitButtonView( + frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 52.adjustedH) + ) init(type: ScenarioEmptyViewType) { self.type = type @@ -33,7 +36,7 @@ final class ScenarioEmptyView: BaseView { override func setStyle() { worryImageView.do { - $0.image = .starWorry + $0.image = type.isHome ? .exclamation : .starWorry $0.contentMode = .scaleAspectFill } titleLabel.do { @@ -55,18 +58,26 @@ final class ScenarioEmptyView: BaseView { subtitleLabel ) if type.isHome { - addSubview(moveButton) + addSubviews( + moveButton, + weatherKitButtonView + ) } } override func setLayout() { worryImageView.snp.makeConstraints { - $0.top.equalToSuperview() + let topInset = type.isHome ? 20.adjustedH : 0 + $0.top.equalToSuperview().inset(topInset) + $0.centerX.equalToSuperview() - $0.size.equalTo(200.adjustedW) + + let imageSize = type.isHome ? 48.adjustedW : 200.adjustedW + $0.size.equalTo(imageSize) } titleLabel.snp.makeConstraints { - $0.top.equalTo(worryImageView.snp.bottom).offset(10.adjustedH) + let topOffset = type.isHome ? 20.adjustedH : 10.adjustedH + $0.top.equalTo(worryImageView.snp.bottom).offset(topOffset) $0.centerX.equalToSuperview() $0.height.equalTo(26.adjustedH) } @@ -81,7 +92,11 @@ final class ScenarioEmptyView: BaseView { $0.centerX.equalToSuperview() $0.width.equalTo(139.adjustedW) $0.height.equalTo(48.adjustedH) - $0.bottom.equalToSuperview() + } + weatherKitButtonView.snp.makeConstraints { + $0.top.equalTo(moveButton.snp.bottom).offset(110.adjustedH) + $0.horizontalEdges.equalToSuperview() + $0.bottom.equalTo(safeAreaLayoutGuide.snp.bottom) } } } diff --git a/BeforeGoing/Presentation/Common/WeatherKitButtonView.swift b/BeforeGoing/Presentation/Common/WeatherKitButtonView.swift new file mode 100644 index 00000000..ca661c9f --- /dev/null +++ b/BeforeGoing/Presentation/Common/WeatherKitButtonView.swift @@ -0,0 +1,35 @@ +// +// WeatherKitButtonView.swift +// BeforeGoing +// +// Created by APPLE on 1/7/26. +// + +import UIKit + +final class WeatherKitButtonView: BaseView { + + private(set) var weatherKitButton = UIButton() + + override func setStyle() { + weatherKitButton.do { + $0.setTitle(" Weather", for: .normal) + $0.setTitleColor(.gray400, for: .normal) + $0.titleLabel?.font = .custom(.bodySMMedium) + $0.titleLabel?.textAlignment = .center + } + } + + override func setUI() { + addSubview(weatherKitButton) + } + + override func setLayout() { + weatherKitButton.snp.makeConstraints { + $0.top.equalToSuperview().inset(20.adjustedH) + $0.bottom.equalToSuperview().inset(30.adjustedH) + $0.centerX.equalToSuperview() + $0.width.equalTo(80.adjustedW) + } + } +} diff --git a/BeforeGoing/Presentation/Enum/ExternalLink.swift b/BeforeGoing/Presentation/Enum/ExternalLink.swift index 0f77c5f2..9738fbb6 100644 --- a/BeforeGoing/Presentation/Enum/ExternalLink.swift +++ b/BeforeGoing/Presentation/Enum/ExternalLink.swift @@ -14,6 +14,7 @@ enum ExternalLink: String { case privacy = "https://fluffy-nectarine-129.notion.site/2824ff02f66080b290c6ce933b8759d7?source=copy_link" case term = "https://fluffy-nectarine-129.notion.site/2824ff02f6608029a52ed13a25059f97?source=copy_link" case notice = "https://fluffy-nectarine-129.notion.site/2a04ff02f6608083af96e52f216f1c88" + case weatherLegal = "https://developer.apple.com/weatherkit/data-source-attribution/" func openURL(for rootViewController: UIViewController) { guard let url = URL(string: self.rawValue) else { diff --git a/BeforeGoing/Presentation/Extensions/UITableViewDragDelegate+.swift b/BeforeGoing/Presentation/Extensions/UITableViewDragDelegate+.swift new file mode 100644 index 00000000..82491eac --- /dev/null +++ b/BeforeGoing/Presentation/Extensions/UITableViewDragDelegate+.swift @@ -0,0 +1,15 @@ +// +// UITableViewDragDelegate+.swift +// BeforeGoing +// +// Created by APPLE on 1/13/26. +// + +import UIKit + +extension UITableViewDragDelegate { + + func provideDragItem() -> [UIDragItem] { + return [UIDragItem(itemProvider: NSItemProvider())] + } +} diff --git a/BeforeGoing/Presentation/Extensions/UITableViewDropDelegate+.swift b/BeforeGoing/Presentation/Extensions/UITableViewDropDelegate+.swift new file mode 100644 index 00000000..bc167c57 --- /dev/null +++ b/BeforeGoing/Presentation/Extensions/UITableViewDropDelegate+.swift @@ -0,0 +1,37 @@ +// +// UITableViewDropDelegate+.swift +// BeforeGoing +// +// Created by APPLE on 1/13/26. +// + +import UIKit + +extension UITableViewDropDelegate { + + func handleDrop( + with coordinator: UITableViewDropCoordinator, + action: (_ sourceSection: Int, _ destinationSection: Int) -> Void + ) { + guard let destinationIndexPath = coordinator.destinationIndexPath else { + return + } + + let destinationSection = destinationIndexPath.section + + for item in coordinator.items { + guard let sourceIndexPath = item.sourceIndexPath else { + return + } + + action(sourceIndexPath.section, destinationSection) + } + } + + func handleDropProposal(dropSessionDidUpdate session: UIDropSession) -> UITableViewDropProposal { + if session.localDragSession != nil { + return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) + } + return UITableViewDropProposal(operation: .cancel, intent: .unspecified) + } +} diff --git a/BeforeGoing/Presentation/Feature/Approach/ViewController/AgreeTermsViewController.swift b/BeforeGoing/Presentation/Feature/Approach/ViewController/AgreeTermsViewController.swift index f03ed8d6..547d58a7 100644 --- a/BeforeGoing/Presentation/Feature/Approach/ViewController/AgreeTermsViewController.swift +++ b/BeforeGoing/Presentation/Feature/Approach/ViewController/AgreeTermsViewController.swift @@ -88,8 +88,26 @@ extension AgreeTermsViewController { } if output.agreeTermsResult { - let nicknameViewController = ViewControllerFactory.shared.makeNicknameViewController() - self.navigationController?.pushViewController(nicknameViewController, animated: false) + guard let isAppleLoginedOutput = try await viewModel.action( + input: .checkLoginMethod + ) as? AgreeItemViewModel.IsAppleLoginedOutput else { + return + } + + switch isAppleLoginedOutput.isAppleLogined { + case .success(let isAppleLogined): + if isAppleLogined { + let onboardoingViewController = ViewControllerFactory.shared.makeOnboardingViewController() + onboardoingViewController.navigationItem.hidesBackButton = true + self.navigationController?.pushViewController(onboardoingViewController, animated: false) + return + } + let nicknameViewController = ViewControllerFactory.shared.makeNicknameViewController() + self.navigationController?.pushViewController(nicknameViewController, animated: false) + + case .failure(let error): + BeforeGoingLogger.error(error) + } return } BeforeGoingLogger.error(BeforeGoingError.agreeTermsFailed) diff --git a/BeforeGoing/Presentation/Feature/Approach/ViewController/LoginViewController.swift b/BeforeGoing/Presentation/Feature/Approach/ViewController/LoginViewController.swift index 2e32f163..9e3e61aa 100644 --- a/BeforeGoing/Presentation/Feature/Approach/ViewController/LoginViewController.swift +++ b/BeforeGoing/Presentation/Feature/Approach/ViewController/LoginViewController.swift @@ -33,7 +33,7 @@ final class LoginViewController: BaseViewController { do { if let output = try await viewModel.action(input: .viewDidLoad) as? LoginViewModel.AutoLoginOutput, output.isSucceed { - moveHome() + moveByNotification() } } catch { BeforeGoingLogger.error(BeforeGoingError.autoLoginFailed) @@ -93,7 +93,7 @@ extension LoginViewController: NetworkRequestable, NetworkRequestErrorHandler { ) as? LoginViewModel.SocialLoginOutput else { return } - output.isRegisteredMember ? moveHome() : moveTerms() + output.isRegisteredMember ? moveByNotification() : moveTerms() } catch (let error) { self.handleError(error) BeforeGoingLogger.error(BeforeGoingError.loginFailed) @@ -107,7 +107,7 @@ extension LoginViewController: NetworkRequestable, NetworkRequestErrorHandler { do { let _ = try await viewModel.action(input: .appleLoginDidTap) viewModel.onAppleLoginPerformed = { [weak self] isMemberRegistered in - isMemberRegistered ? self?.moveHome() : self?.moveTerms() + isMemberRegistered ? self?.moveByNotification() : self?.moveTerms() } } catch (let error) { self.handleError(error) @@ -116,7 +116,7 @@ extension LoginViewController: NetworkRequestable, NetworkRequestErrorHandler { } } - private func moveHome() { + private func moveByNotification() { if let request = AuthManager.shared.pendingNotificationRequest { AuthManager.shared.pendingNotificationRequest = nil @@ -126,24 +126,26 @@ extension LoginViewController: NetworkRequestable, NetworkRequestErrorHandler { switch notificationIdentifier { case .pushNotice : - ViewControllerUtil.replaceRootViewController( - to: BottomNavigationViewController(scenarioTitle: request.content.title) + replaceViewController( + to: BottomNavigationViewController( + scenarioTitle: request.content.title + ) ) - case .callNotice(let sequence): - ViewControllerUtil.replaceRootViewController( + replaceViewController( to: NotificationViewController( notificationViewType: .init(sequence: sequence), content: request.content, identifier: notificationIdentifier.identifier ) ) - case .terminate: - ViewControllerUtil.replaceRootViewController(to: BottomNavigationViewController()) + replaceViewController(to: BottomNavigationViewController()) } + return } + let viewController = BottomNavigationViewController() ViewControllerUtil.replaceRootViewController(to: viewController) } @@ -153,4 +155,8 @@ extension LoginViewController: NetworkRequestable, NetworkRequestErrorHandler { viewController.navigationItem.hidesBackButton = true self.navigationController?.pushViewController(viewController, animated: true) } + + private func replaceViewController(to viewController: UIViewController) { + ViewControllerUtil.replaceRootViewController(to: viewController) + } } diff --git a/BeforeGoing/Presentation/Feature/Approach/ViewModel/AgreeItemViewModel.swift b/BeforeGoing/Presentation/Feature/Approach/ViewModel/AgreeItemViewModel.swift index 028622dc..1692d147 100644 --- a/BeforeGoing/Presentation/Feature/Approach/ViewModel/AgreeItemViewModel.swift +++ b/BeforeGoing/Presentation/Feature/Approach/ViewModel/AgreeItemViewModel.swift @@ -11,11 +11,13 @@ final class AgreeItemViewModel: ViewModeling { private var agreeItems = AgreeItem.allCases private var checkBoxStates: [AgreeItem : CheckBoxState] = [:] - private let useCase: SendAgreeTermsType + private let sendAgreeUseCase: SendAgreeTermsType + private let isAppleLoginedUseCase: IsAppleLoginType enum Input { case nextButtonDidTap case initTerms + case checkLoginMethod } typealias Output = AgreeItemOutput @@ -26,8 +28,17 @@ final class AgreeItemViewModel: ViewModeling { struct EmptyOutput: AgreeItemOutput {} - init(useCase: SendAgreeTermsType) { - self.useCase = useCase + struct IsAppleLoginedOutput: AgreeItemOutput { + let isAppleLogined: Result + } + + init( + sendAgreeUseCase: SendAgreeTermsType, + isAppleLoginedUseCase: IsAppleLoginType + ) { + self.sendAgreeUseCase = sendAgreeUseCase + self.isAppleLoginedUseCase = isAppleLoginedUseCase + agreeItems.forEach { checkBoxStates[$0] = .unchecked } } @@ -35,7 +46,7 @@ final class AgreeItemViewModel: ViewModeling { switch input { case .nextButtonDidTap: do { - try await useCase.execute( + try await sendAgreeUseCase.execute( termsOfServiceAgreed: matchState(item: .isTermsOfServiceAgreed), privacyPolicyAgreed: matchState(item: .isPrivacyPolicyAgreed), isOver14: matchState(item: .isOverFourteen), @@ -47,9 +58,20 @@ final class AgreeItemViewModel: ViewModeling { BeforeGoingLogger.error(error) return TermsOutput(agreeTermsResult: false) } + case .initTerms: agreeItems.forEach { checkBoxStates[$0] = .unchecked } return EmptyOutput() + + case .checkLoginMethod: + let result = isAppleLoginedUseCase.execute() + + switch result { + case .some(let isAppleLogined): + return IsAppleLoginedOutput(isAppleLogined: .success(isAppleLogined)) + case .none: + return IsAppleLoginedOutput(isAppleLogined: .failure(.notFoundProvider)) + } } } } diff --git a/BeforeGoing/Presentation/Feature/Approach/ViewModel/LoginViewModel.swift b/BeforeGoing/Presentation/Feature/Approach/ViewModel/LoginViewModel.swift index 96d4531d..efba1916 100644 --- a/BeforeGoing/Presentation/Feature/Approach/ViewModel/LoginViewModel.swift +++ b/BeforeGoing/Presentation/Feature/Approach/ViewModel/LoginViewModel.swift @@ -11,6 +11,12 @@ import AuthenticationServices final class LoginViewModel: NSObject, ViewModeling { + private let personNameFormatter: PersonNameComponentsFormatter = { + let formatter = PersonNameComponentsFormatter() + formatter.style = .default + return formatter + }() + private let autoLoginUseCase: AutoLoginType private let loginUseCase: LoginType private let getLastLoginUseCase: GetLastLoginType @@ -67,6 +73,7 @@ final class LoginViewModel: NSObject, ViewModeling { case .appleLoginDidTap: let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() + request.requestedScopes = [.fullName] let nonce = try await loginUseCase.requestNonce(provider: .apple) request.nonce = nonce @@ -94,10 +101,16 @@ extension LoginViewModel: ASAuthorizationControllerDelegate { let idToken = String(data: identityTokenData, encoding: .utf8) else { return } + + let name = credential.fullName.flatMap { personNameFormatter.string(from: $0) } Task { do { - let isMemberRegistered = try await loginUseCase.login(provider: .apple, idToken: idToken) + let isMemberRegistered = try await loginUseCase.login( + provider: .apple, + idToken: idToken, + name: name + ) onAppleLoginPerformed?(isMemberRegistered) } catch (let error) { BeforeGoingLogger.error(error) diff --git a/BeforeGoing/Presentation/Feature/Home/View/UserScenarioModalView.swift b/BeforeGoing/Presentation/Feature/Home/View/UserScenarioModalView.swift index 924fdd51..89e53c4e 100644 --- a/BeforeGoing/Presentation/Feature/Home/View/UserScenarioModalView.swift +++ b/BeforeGoing/Presentation/Feature/Home/View/UserScenarioModalView.swift @@ -24,6 +24,9 @@ final class UserScenarioModalView: BaseView { private(set) var addTaskButton = UIButton() private(set) var deleteTaskButton = UIButton() private(set) var listTableView = UITableView() + private(set) lazy var weatherKitButtonView = WeatherKitButtonView( + frame: CGRect(x: 0, y: 0, width: 80.adjustedW, height: 52.adjustedH) + ) init() { super.init(frame: .zero) @@ -57,6 +60,7 @@ final class UserScenarioModalView: BaseView { } listTableView.do { $0.separatorStyle = .none + $0.tableFooterView = weatherKitButtonView } } @@ -188,19 +192,27 @@ extension UserScenarioModalView { listTableView, taskTextField, addTaskButton, - deleteTaskButton - ].forEach { $0.removeFromSuperview() } + deleteTaskButton, + weatherKitButtonView + ].forEach { $0.isHidden = true } addSubview(emptyView) + emptyView.isHidden = false emptyView.snp.makeConstraints { $0.top.equalToSuperview().inset(80.adjustedH) $0.horizontalEdges.equalToSuperview().inset(20.adjustedW) - $0.height.equalTo(285.adjustedH) + $0.bottom.equalToSuperview() } } func replaceModalView() { - emptyView.removeFromSuperview() - setUI() - setLayout() + emptyView.isHidden = true + [ + headerView, + listTableView, + taskTextField, + addTaskButton, + deleteTaskButton, + weatherKitButtonView + ].forEach { $0.isHidden = false } } } diff --git a/BeforeGoing/Presentation/Feature/Home/ViewController/HomeViewController.swift b/BeforeGoing/Presentation/Feature/Home/ViewController/HomeViewController.swift index dae8243e..91cbea54 100644 --- a/BeforeGoing/Presentation/Feature/Home/ViewController/HomeViewController.swift +++ b/BeforeGoing/Presentation/Feature/Home/ViewController/HomeViewController.swift @@ -18,7 +18,7 @@ final class HomeViewController: BaseViewController { private let locationManager = CLLocationManager() private var homeDate: String? - private var memberName: String? + private var memberName: String = "워리" private var scenarioTitle: String? init( @@ -50,10 +50,9 @@ final class HomeViewController: BaseViewController { override func viewDidLoad() { super.viewDidLoad() - - setLocationManager() - requestDate() - updateWeatherInformation(date: DateUtil.getCurrentDate()) + + locationManager.delegate = self + updateDateAndWeather() } override func touchesBegan(_ touches: Set, with event: UIEvent?) { @@ -94,6 +93,16 @@ final class HomeViewController: BaseViewController { action: #selector(moveButtonDidTap), for: .touchUpInside ) + [ + rootView.modalView.weatherKitButtonView.weatherKitButton, + rootView.modalView.emptyView.weatherKitButtonView.weatherKitButton + ].forEach { + $0.addTarget( + self, + action: #selector(weatherKitButtonDidTap), + for: .touchUpInside + ) + } } override func setDelegate() { @@ -105,29 +114,55 @@ final class HomeViewController: BaseViewController { } } - private func requestDate() { + private func updateDateAndWeather() { Task { - do { - guard let result = try await homeViewModel.action( - input: .requestDate - ) as? HomeViewModel.DateOutput, - let monthAndDay = DateUtil.toMonthAndDay(date: result.date) - else { - return + if #available(iOS 17.0, *) { + try await withThrowingDiscardingTaskGroup { group in + group.addTask { [weak self] in + try await self?.requestDate() + } + + group.addTask { [weak self] in + try await self?.updateWeatherInformation(date: DateUtil.getCurrentDate()) + } + } + } else { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { [weak self] in + try await self?.requestDate() + } + + group.addTask { [weak self] in + try await self?.updateWeatherInformation(date: DateUtil.getCurrentDate()) + } + + try await group.waitForAll() } - self.homeDate = result.date - rootView.headerView.updateDateUI(date: result.date) - rootView.modalView.updateTaskField( - isEnabled: true, - text: "\(monthAndDay)의 미션을 추가해요" - ) - } catch (let error) { - self.handleError(error) - BeforeGoingLogger.error(error) } } } + private func requestDate() async throws { + do { + guard let result = try await homeViewModel.action( + input: .requestDate + ) as? HomeViewModel.DateOutput, + let monthAndDay = DateUtil.toMonthAndDay(date: result.date) + else { + return + } + self.homeDate = result.date + rootView.headerView.updateDateUI(date: result.date) + rootView.modalView.updateTaskField( + isEnabled: true, + text: "\(monthAndDay)의 미션을 추가해요" + ) + } catch (let error) { + self.handleError(error) + BeforeGoingLogger.error(error) + } + } + private func checkLoactionAuthorization() { switch locationManager.authorizationStatus { case .authorizedAlways, .authorizedWhenInUse: @@ -194,32 +229,26 @@ final class HomeViewController: BaseViewController { } } - private func updateWeatherInformation(date: Date) { + private func updateWeatherInformation(date: Date) async throws { guard let latitude = locationManager.location?.coordinate.latitude, let longitude = locationManager.location?.coordinate.longitude else { return } - Task { - try await getMemberName() - - guard let memberName else { - return - } - - guard let result = try await homeViewModel.action( - input: .requestWeather(date: date, memberName: memberName, latitude: latitude, longitude: longitude) - ) as? HomeViewModel.WeatherOutput else { - return - } - - switch result.weatherResult { - case .success(let weatherInformation): - self.rootView.headerView.updateWeatherUI(information: weatherInformation) - case .failure(let error): - self.handleError(error) - BeforeGoingLogger.error(error) - } + try await getMemberName() + + guard let result = try await homeViewModel.action( + input: .requestWeather(date: date, memberName: memberName, latitude: latitude, longitude: longitude) + ) as? HomeViewModel.WeatherOutput else { + return + } + + switch result.weatherResult { + case .success(let weatherInformation): + self.rootView.headerView.updateWeatherUI(information: weatherInformation) + case .failure(let error): + self.handleError(error) + BeforeGoingLogger.error(error) } } @@ -238,11 +267,23 @@ final class HomeViewController: BaseViewController { } private func setGesture() { - let tapGesture = UITapGestureRecognizer( + let calendarTapGesture = UITapGestureRecognizer( target: self, action: #selector(viewCalendarButtonDidTap) ) - rootView.headerView.dateStackView.addGestureRecognizer(tapGesture) + rootView.headerView.dateStackView.addGestureRecognizer(calendarTapGesture) + + [ + rootView.modalView.weatherKitButtonView, + rootView.modalView.emptyView.weatherKitButtonView + ].forEach { + let weatherKitLogoTapGesture = UITapGestureRecognizer( + target: self, + action: #selector(weatherKitButtonDidTap) + ) + $0.isUserInteractionEnabled = true + $0.addGestureRecognizer(weatherKitLogoTapGesture) + } } private func setGesture(scenarios: [ScenarioEntity]) { @@ -259,12 +300,6 @@ final class HomeViewController: BaseViewController { ) } } - - private func setLocationManager() { - locationManager.do { - $0.delegate = self - } - } } extension HomeViewController: ToastPresentable { @@ -291,7 +326,9 @@ extension HomeViewController: ToastPresentable { monthAndDay: monthAndDay ) - updateWeatherInformation(date: date) + Task { + try await self.updateWeatherInformation(date: date) + } } calendarViewController.onDismiss = { [weak self] in guard let homeDate = self?.homeDate, @@ -410,6 +447,11 @@ extension HomeViewController: ToastPresentable { pushManageScenario(navigationController: navigationController) } + @objc + private func weatherKitButtonDidTap() { + ExternalLink.weatherLegal.openURL(for: self) + } + private func initSelectedDate(calendarViewController: CalendarViewController) { if let homeDateString = self.homeDate, let date = DateUtil.toDate(dateString: homeDateString) { @@ -658,8 +700,11 @@ extension HomeViewController: UITableViewDataSource { } private func createSwipeAction(deleteAction: UIContextualAction) -> UISwipeActionsConfiguration { - let config = UISwipeActionsConfiguration(actions: [deleteAction]) - config.performsFirstActionWithFullSwipe = false - return config + let swipeActionsConfig: UISwipeActionsConfiguration = { + let config = UISwipeActionsConfiguration(actions: [deleteAction]) + config.performsFirstActionWithFullSwipe = false + return config + }() + return swipeActionsConfig } } diff --git a/BeforeGoing/Presentation/Feature/Home/ViewModel/HomeViewModel.swift b/BeforeGoing/Presentation/Feature/Home/ViewModel/HomeViewModel.swift index bd1e010f..e5b0800e 100644 --- a/BeforeGoing/Presentation/Feature/Home/ViewModel/HomeViewModel.swift +++ b/BeforeGoing/Presentation/Feature/Home/ViewModel/HomeViewModel.swift @@ -104,7 +104,7 @@ final class HomeViewModel: ViewModeling { func action(input: Input) async throws -> Output { switch input { case .requestName: - let memberName = getMemberNameUseCase.execute() + let memberName = try await getMemberNameUseCase.execute() return MemberNameOutput(memberName: memberName) case .requestDate: diff --git a/BeforeGoing/Presentation/Feature/Onboarding/ViewController/OnboardingViewController.swift b/BeforeGoing/Presentation/Feature/Onboarding/ViewController/OnboardingViewController.swift index 448fdfd9..59b85ad4 100644 --- a/BeforeGoing/Presentation/Feature/Onboarding/ViewController/OnboardingViewController.swift +++ b/BeforeGoing/Presentation/Feature/Onboarding/ViewController/OnboardingViewController.swift @@ -10,16 +10,6 @@ import UIKit final class OnboardingViewController: BaseViewController { private let rootView = OnboardingView(step: .first) - private let viewModel: OnboardingViewModel - - init(viewModel: OnboardingViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } override func loadView() { view = rootView @@ -44,12 +34,9 @@ extension OnboardingViewController { private func bottomButtonDidTap() { let updateResult = rootView.moveFront() if !updateResult { - let result = viewModel.action(input: .startButtonDidTap) - if result.isCompletedOnboarding { - let viewController = ViewControllerFactory.shared.makeAlarmAuthorizationViewController() - viewController.navigationItem.hidesBackButton = true - self.navigationController?.pushViewController(viewController, animated: false) - } + let viewController = ViewControllerFactory.shared.makeAlarmAuthorizationViewController() + viewController.navigationItem.hidesBackButton = true + self.navigationController?.pushViewController(viewController, animated: false) } } } diff --git a/BeforeGoing/Presentation/Feature/Onboarding/ViewModel/OnboardingViewModel.swift b/BeforeGoing/Presentation/Feature/Onboarding/ViewModel/OnboardingViewModel.swift deleted file mode 100644 index 0c4e1abb..00000000 --- a/BeforeGoing/Presentation/Feature/Onboarding/ViewModel/OnboardingViewModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// OnboardingViewModel.swift -// BeforeGoing -// -// Created by APPLE on 11/13/25. -// - -final class OnboardingViewModel: ViewModeling { - - private let useCase: SaveOnboardingCompletedType - - init(useCase: SaveOnboardingCompletedType) { - self.useCase = useCase - } - - enum Input { - case startButtonDidTap - } - - struct Output { - let isCompletedOnboarding: Bool - } - - func action(input: Input) -> Output { - switch input { - case .startButtonDidTap: - let isSaved = useCase.saveOnboardingCompleted() - return .init(isCompletedOnboarding: isSaved) - } - } -} diff --git a/BeforeGoing/Presentation/Feature/Scenario/ViewController/ManageScenarioViewController.swift b/BeforeGoing/Presentation/Feature/Scenario/ViewController/ManageScenarioViewController.swift index 405e0e72..b472532f 100644 --- a/BeforeGoing/Presentation/Feature/Scenario/ViewController/ManageScenarioViewController.swift +++ b/BeforeGoing/Presentation/Feature/Scenario/ViewController/ManageScenarioViewController.swift @@ -123,7 +123,7 @@ extension ManageScenarioViewController: UITableViewDelegate { } } -extension ManageScenarioViewController: UITableViewDataSource { +extension ManageScenarioViewController: UITableViewDataSource, TableViewSwipeAction { func numberOfSections(in tableView: UITableView) -> Int { return viewModel.templateCount @@ -152,48 +152,10 @@ extension ManageScenarioViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - - let deleteAction = createDeleteAction(tableView: tableView, indexPath: indexPath) - let largeConfig = createLargeConfig() - setDeleteActionStyle(deleteAction: deleteAction, largeConfig: largeConfig) - - let config = createSwipeAction(deleteAction: deleteAction) - - return config - } - - private func createDeleteAction(tableView: UITableView, indexPath: IndexPath) -> UIContextualAction { - return UIContextualAction( - style: .normal, - title: nil - ) { [weak self] (_, view, completion) in + return createSwipeActionConfig(tableView: tableView, indexPath: indexPath) { [weak self] in let _ = self?.viewModel.removeScenarioType(at: indexPath.section) tableView.deleteSections(IndexSet(integer: indexPath.section), with: .automatic) - completion(true) - } - } - - private func createLargeConfig() -> UIImage.SymbolConfiguration { - return UIImage.SymbolConfiguration(pointSize: 12.0, weight: .bold, scale: .large) - } - - private func setDeleteActionStyle( - deleteAction: UIContextualAction, - largeConfig: UIImage.SymbolConfiguration - ) { - deleteAction.do { - $0.backgroundColor = .white - $0.image = UIImage( - systemName: "trash", - withConfiguration: largeConfig - )?.withTintColor(.white, renderingMode: .alwaysTemplate).addBackgroundCircle(.warning500) - } - } - - private func createSwipeAction(deleteAction: UIContextualAction) -> UISwipeActionsConfiguration { - let config = UISwipeActionsConfiguration(actions: [deleteAction]) - config.performsFirstActionWithFullSwipe = false - return config + } } } @@ -204,23 +166,23 @@ extension ManageScenarioViewController: UITableViewDragDelegate { itemsForBeginning session: any UIDragSession, at indexPath: IndexPath ) -> [UIDragItem] { - return [UIDragItem(itemProvider: NSItemProvider())] + provideDragItem() } } extension ManageScenarioViewController: UITableViewDropDelegate { - func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { - guard let destinationIndexPath = coordinator.destinationIndexPath else { return } - let destinationSection = destinationIndexPath.section - - for item in coordinator.items { - guard let sourceIndexPath = item.sourceIndexPath, - let movedSection = viewModel.removeScenarioType(at: sourceIndexPath.section) else { + func tableView( + _ tableView: UITableView, + performDropWith coordinator: UITableViewDropCoordinator + ) { + handleDrop(with: coordinator) { sourceSection, destinationSection in + guard let movedSection = viewModel.removeScenarioType(at: sourceSection) else { return } viewModel.addScenarioType(movedSection, at: destinationSection) } + tableView.reloadData() } @@ -229,9 +191,6 @@ extension ManageScenarioViewController: UITableViewDropDelegate { dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath? ) -> UITableViewDropProposal { - if session.localDragSession != nil { - return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) - } - return UITableViewDropProposal(operation: .cancel, intent: .unspecified) + handleDropProposal(dropSessionDidUpdate: session) } } diff --git a/BeforeGoing/Presentation/Feature/Scenario/ViewController/MyScenarioViewController.swift b/BeforeGoing/Presentation/Feature/Scenario/ViewController/MyScenarioViewController.swift index 13b5ac7d..8ef9a24d 100644 --- a/BeforeGoing/Presentation/Feature/Scenario/ViewController/MyScenarioViewController.swift +++ b/BeforeGoing/Presentation/Feature/Scenario/ViewController/MyScenarioViewController.swift @@ -182,7 +182,7 @@ extension MyScenarioViewController: UITableViewDelegate { } } -extension MyScenarioViewController: UITableViewDataSource { +extension MyScenarioViewController: UITableViewDataSource, TableViewSwipeAction { func numberOfSections(in tableView: UITableView) -> Int { return getScenariosViewModel.scenariosCount @@ -209,26 +209,7 @@ extension MyScenarioViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - - let deleteAction = createDeleteAction(tableView: tableView, indexPath: indexPath) - let largeConfig = createLargeConfig() - setDeleteActionStyle(deleteAction: deleteAction, largeConfig: largeConfig) - - let config = createSwipeAction(deleteAction: deleteAction) - return config - } - - private func bindCell(to cell: ScenarioListItemCell, section: Int) { - let name = getScenariosViewModel.getScenarioName(section: section) - let noticeInformation = getScenariosViewModel.getNotificationInformation(section: section) - cell.bind(name: name, noticeInformation: noticeInformation) - } - - private func createDeleteAction(tableView: UITableView, indexPath: IndexPath) -> UIContextualAction { - return UIContextualAction( - style: .normal, - title: nil - ) { [weak self] (_, view, completion) in + return createSwipeActionConfig(tableView: tableView, indexPath: indexPath) { [weak self] in Task { guard let self = self else { return } @@ -248,33 +229,14 @@ extension MyScenarioViewController: UITableViewDataSource { self.handleError(error) BeforeGoingLogger.error(error) } - completion(true) } } } - - private func createLargeConfig() -> UIImage.SymbolConfiguration { - return UIImage.SymbolConfiguration(pointSize: 12.0, weight: .bold, scale: .large) - } - - private func setDeleteActionStyle( - deleteAction: UIContextualAction, - largeConfig: UIImage.SymbolConfiguration - ) { - deleteAction.do { - $0.backgroundColor = .white - $0.image = UIImage( - systemName: "trash", - withConfiguration: largeConfig - )?.withTintColor(.white, renderingMode: .alwaysTemplate).addBackgroundCircle(.warning500) - } - } - - private func createSwipeAction(deleteAction: UIContextualAction) -> UISwipeActionsConfiguration { - let config = UISwipeActionsConfiguration(actions: [deleteAction]) - config.performsFirstActionWithFullSwipe = false - return config + private func bindCell(to cell: ScenarioListItemCell, section: Int) { + let name = getScenariosViewModel.getScenarioName(section: section) + let noticeInformation = getScenariosViewModel.getNotificationInformation(section: section) + cell.bind(name: name, noticeInformation: noticeInformation) } } @@ -285,23 +247,21 @@ extension MyScenarioViewController: UITableViewDragDelegate { itemsForBeginning session: any UIDragSession, at indexPath: IndexPath ) -> [UIDragItem] { - return [UIDragItem(itemProvider: NSItemProvider())] + provideDragItem() } } extension MyScenarioViewController: UITableViewDropDelegate { - func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { - guard let destinationIndexPath = coordinator.destinationIndexPath else { return } - let destinationSection = destinationIndexPath.section - - for item in coordinator.items { - guard let sourceIndexPath = item.sourceIndexPath else { continue } - let sourceSection = sourceIndexPath.section - + func tableView( + _ tableView: UITableView, + performDropWith coordinator: UITableViewDropCoordinator + ) { + handleDrop(with: coordinator) { sourceSection, destinationSection in moveScenario(originalAt: sourceSection, destinationAt: destinationSection) updateScenarioOrder(originalAt: sourceSection, destinationAt: destinationSection) } + tableView.reloadData() } @@ -310,10 +270,7 @@ extension MyScenarioViewController: UITableViewDropDelegate { dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath? ) -> UITableViewDropProposal { - if session.localDragSession != nil { - return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) - } - return UITableViewDropProposal(operation: .cancel, intent: .unspecified) + handleDropProposal(dropSessionDidUpdate: session) } private func moveScenario(originalAt: Int, destinationAt: Int) { diff --git a/BeforeGoing/Presentation/Feature/Scenario/ViewController/SettingScenarioViewController.swift b/BeforeGoing/Presentation/Feature/Scenario/ViewController/SettingScenarioViewController.swift index a9a15b1b..03e088ff 100644 --- a/BeforeGoing/Presentation/Feature/Scenario/ViewController/SettingScenarioViewController.swift +++ b/BeforeGoing/Presentation/Feature/Scenario/ViewController/SettingScenarioViewController.swift @@ -300,8 +300,7 @@ extension SettingScenarioViewController: ToastPresentable { Task { do { - guard let scenarioNames, - !scenarioNames.contains(scenarioName) else { + if let scenarioNames, scenarioNames.contains(scenarioName) { self.presentToastMessage(type: .duplicateScenario) return } @@ -394,7 +393,7 @@ extension SettingScenarioViewController: UITableViewDelegate { } } -extension SettingScenarioViewController: UITableViewDataSource { +extension SettingScenarioViewController: UITableViewDataSource, TableViewSwipeAction { func numberOfSections(in tableView: UITableView) -> Int { return addScenarioViewModel.missionsCount @@ -423,73 +422,39 @@ extension SettingScenarioViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - - let deleteAction = createDeleteAction(tableView: tableView, indexPath: indexPath) - let largeConfig = createLargeConfig() - setDeleteActionStyle(deleteAction: deleteAction, largeConfig: largeConfig) - let config = createSwipeAction(deleteAction: deleteAction) - return config - } - - private func createDeleteAction(tableView: UITableView, indexPath: IndexPath) -> UIContextualAction { - return UIContextualAction( - style: .normal, - title: nil - ) { [weak self] (_, view, completion) in + return createSwipeActionConfig(tableView: tableView, indexPath: indexPath) { [weak self] in guard let self = self else { return } let _ = self.addScenarioViewModel.removeMission(at: indexPath.section) tableView.deleteSections(IndexSet(integer: indexPath.section), with: .automatic) self.checkNextButtonState() self.rootView.settingMissionView.updateMissionCount(self.addScenarioViewModel.missionsCount) - completion(true) - } - } - - private func createLargeConfig() -> UIImage.SymbolConfiguration { - return UIImage.SymbolConfiguration(pointSize: 12.0, weight: .bold, scale: .large) - } - - private func setDeleteActionStyle( - deleteAction: UIContextualAction, - largeConfig: UIImage.SymbolConfiguration - ) { - deleteAction.do { - $0.backgroundColor = .white - $0.image = UIImage( - systemName: "trash", - withConfiguration: largeConfig - )?.withTintColor(.white, renderingMode: .alwaysTemplate).addBackgroundCircle(.warning500) - } - } - - private func createSwipeAction(deleteAction: UIContextualAction) -> UISwipeActionsConfiguration { - let config = UISwipeActionsConfiguration(actions: [deleteAction]) - config.performsFirstActionWithFullSwipe = false - return config + } } } extension SettingScenarioViewController: UITableViewDragDelegate { - func tableView(_ tableView: UITableView, itemsForBeginning session: any UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { - return [UIDragItem(itemProvider: NSItemProvider())] + func tableView( + _ tableView: UITableView, + itemsForBeginning session: any UIDragSession, + at indexPath: IndexPath + ) -> [UIDragItem] { + provideDragItem() } } extension SettingScenarioViewController: UITableViewDropDelegate { - func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { - guard let destinationIndexPath = coordinator.destinationIndexPath else { return } - let destinationSection = destinationIndexPath.section - - for item in coordinator.items { - guard let sourceIndexPath = item.sourceIndexPath else { continue } - let sourceSection = sourceIndexPath.section - + func tableView( + _ tableView: UITableView, + performDropWith coordinator: UITableViewDropCoordinator + ) { + handleDrop(with: coordinator) { sourceSection, destinationSection in let movedSection = addScenarioViewModel.removeMission(at: sourceSection) addScenarioViewModel.addMission(movedSection, at: destinationSection) } + tableView.reloadData() } @@ -498,9 +463,6 @@ extension SettingScenarioViewController: UITableViewDropDelegate { dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath? ) -> UITableViewDropProposal { - if session.localDragSession != nil { - return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) - } - return UITableViewDropProposal(operation: .cancel, intent: .unspecified) + handleDropProposal(dropSessionDidUpdate: session) } } diff --git a/BeforeGoing/Presentation/Feature/Setting/View/Main/SettingView.swift b/BeforeGoing/Presentation/Feature/Setting/View/Main/SettingView.swift index f64aff90..2b151f5b 100644 --- a/BeforeGoing/Presentation/Feature/Setting/View/Main/SettingView.swift +++ b/BeforeGoing/Presentation/Feature/Setting/View/Main/SettingView.swift @@ -18,6 +18,7 @@ final class SettingView: BaseView { private(set) var policyView = PolicyView() override func setStyle() { + backgroundColor = .white titleLabel.do { $0.text = "설정" $0.textColor = .gray900 diff --git a/BeforeGoing/Presentation/Feature/Setting/View/ModifyName/ModifyNameView.swift b/BeforeGoing/Presentation/Feature/Setting/View/ModifyName/ModifyNameView.swift index acd16b6a..83777926 100644 --- a/BeforeGoing/Presentation/Feature/Setting/View/ModifyName/ModifyNameView.swift +++ b/BeforeGoing/Presentation/Feature/Setting/View/ModifyName/ModifyNameView.swift @@ -19,6 +19,7 @@ final class ModifyNameView: BaseView { private(set) var confirmButton = CustomButton(state: .enableLongButton, title: "확인") override func setStyle() { + backgroundColor = .white nameLabel.do { $0.text = "이름" $0.textColor = .gray600 diff --git a/BeforeGoing/Presentation/Feature/Setting/View/Profile/ProfileView.swift b/BeforeGoing/Presentation/Feature/Setting/View/Profile/ProfileView.swift index 7b1c0c6a..31646c2f 100644 --- a/BeforeGoing/Presentation/Feature/Setting/View/Profile/ProfileView.swift +++ b/BeforeGoing/Presentation/Feature/Setting/View/Profile/ProfileView.swift @@ -18,6 +18,7 @@ final class ProfileView: BaseView { private(set) var withdrawView = ProfileFeatureView(title: "회원탈퇴") override func setStyle() { + backgroundColor = .white worryImageView.do { $0.image = .profile } diff --git a/BeforeGoing/Presentation/Feature/Setting/ViewModel/ProfileViewModel.swift b/BeforeGoing/Presentation/Feature/Setting/ViewModel/ProfileViewModel.swift index 69115ddb..4f68fd46 100644 --- a/BeforeGoing/Presentation/Feature/Setting/ViewModel/ProfileViewModel.swift +++ b/BeforeGoing/Presentation/Feature/Setting/ViewModel/ProfileViewModel.swift @@ -46,7 +46,7 @@ final class ProfileViewModel: ViewModeling { func action(input: Input) async throws -> Output { switch input { case .viewWillAppear: - let name = getMemberNameUseCase.execute() + let name = try await getMemberNameUseCase.execute() return MemberNameOutput(name: name) case .logoutButtonDidTap: do { diff --git a/BeforeGoing/Presentation/PresentationDependencyAssembler.swift b/BeforeGoing/Presentation/PresentationDependencyAssembler.swift index 06dace06..f786e0d6 100644 --- a/BeforeGoing/Presentation/PresentationDependencyAssembler.swift +++ b/BeforeGoing/Presentation/PresentationDependencyAssembler.swift @@ -41,6 +41,11 @@ struct PresentationDependencyAssembler: DependencyAssembler { fatalError() } + guard let isAppleLoginedUseCase = DIContainer.shared.resolve(type: IsAppleLoginType.self) else { + BeforeGoingLogger.error(BeforeGoingError.diContainerError) + fatalError() + } + guard let updatePushNoticeUseCase = DIContainer.shared.resolve(type: UpdatePushNoticeType.self) else { BeforeGoingLogger.error(BeforeGoingError.diContainerError) fatalError() @@ -126,14 +131,10 @@ struct PresentationDependencyAssembler: DependencyAssembler { fatalError() } - guard let saveOnboardingCompletedUseCase = DIContainer.shared.resolve(type: SaveOnboardingCompletedType.self) else { - BeforeGoingLogger.error(BeforeGoingError.diContainerError) - fatalError() - } - DIContainer.shared.register( AgreeItemViewModel( - useCase: agreeTermsUseCase + sendAgreeUseCase: agreeTermsUseCase, + isAppleLoginedUseCase: isAppleLoginedUseCase ) ) DIContainer.shared.register( @@ -207,10 +208,5 @@ struct PresentationDependencyAssembler: DependencyAssembler { ) ) DIContainer.shared.register(ManageScenarioViewModel()) - DIContainer.shared.register( - OnboardingViewModel( - useCase: saveOnboardingCompletedUseCase - ) - ) } } diff --git a/BeforeGoing/Presentation/Protocol/TableViewSwipeAction.swift b/BeforeGoing/Presentation/Protocol/TableViewSwipeAction.swift new file mode 100644 index 00000000..03e31b53 --- /dev/null +++ b/BeforeGoing/Presentation/Protocol/TableViewSwipeAction.swift @@ -0,0 +1,73 @@ +// +// TableViewSwipeAction.swift +// BeforeGoing +// +// Created by APPLE on 1/13/26. +// + +import UIKit + +protocol TableViewSwipeAction: AnyObject { + func createSwipeActionConfig( + tableView: UITableView, + indexPath: IndexPath, + action: @escaping () -> Void + ) -> UISwipeActionsConfiguration +} + +extension TableViewSwipeAction where Self: BaseViewController { + + func createSwipeActionConfig( + tableView: UITableView, + indexPath: IndexPath, + action: @escaping () -> Void + ) -> UISwipeActionsConfiguration { + let deleteAction = createDeleteAction( + tableView: tableView, + indexPath: indexPath) { + action() + } + let largeConfig = createLargeConfig() + setDeleteActionStyle(deleteAction: deleteAction, largeConfig: largeConfig) + let config = createSwipeAction(deleteAction: deleteAction) + + return config + } + + private func createSwipeAction(deleteAction: UIContextualAction) -> UISwipeActionsConfiguration { + let config = UISwipeActionsConfiguration(actions: [deleteAction]) + config.performsFirstActionWithFullSwipe = false + return config + } + + private func createDeleteAction( + tableView: UITableView, + indexPath: IndexPath, + action: @escaping () -> Void + ) -> UIContextualAction { + return UIContextualAction( + style: .normal, + title: nil + ) { (_, view, completion) in + action() + completion(true) + } + } + + private func createLargeConfig() -> UIImage.SymbolConfiguration { + return UIImage.SymbolConfiguration(pointSize: 12.0, weight: .bold, scale: .large) + } + + private func setDeleteActionStyle( + deleteAction: UIContextualAction, + largeConfig: UIImage.SymbolConfiguration + ) { + deleteAction.do { + $0.backgroundColor = .white + $0.image = UIImage( + systemName: "trash", + withConfiguration: largeConfig + )?.withTintColor(.white, renderingMode: .alwaysTemplate).addBackgroundCircle(.warning500) + } + } +} diff --git a/BeforeGoing/Presentation/ViewControllerFactory.swift b/BeforeGoing/Presentation/ViewControllerFactory.swift index 1cfbef08..c2534a5b 100644 --- a/BeforeGoing/Presentation/ViewControllerFactory.swift +++ b/BeforeGoing/Presentation/ViewControllerFactory.swift @@ -35,8 +35,7 @@ final class ViewControllerFactory { } func makeOnboardingViewController() -> OnboardingViewController { - let viewModel = resolveViewModel(OnboardingViewModel.self) - return OnboardingViewController(viewModel: viewModel) + return .init() } func makeModifyNicknameViewController() -> ModifyNameViewController { diff --git a/BeforeGoing/Resource/Assets.xcassets/Icon/exclamation.imageset/Contents.json b/BeforeGoing/Resource/Assets.xcassets/Icon/exclamation.imageset/Contents.json new file mode 100644 index 00000000..129e2a45 --- /dev/null +++ b/BeforeGoing/Resource/Assets.xcassets/Icon/exclamation.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "exclamation.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BeforeGoing/Resource/Assets.xcassets/Icon/exclamation.imageset/exclamation.png b/BeforeGoing/Resource/Assets.xcassets/Icon/exclamation.imageset/exclamation.png new file mode 100644 index 00000000..65a965ff Binary files /dev/null and b/BeforeGoing/Resource/Assets.xcassets/Icon/exclamation.imageset/exclamation.png differ diff --git a/BeforeGoing/Resource/Info.plist b/BeforeGoing/Resource/Info.plist index 5698295d..49dce960 100644 --- a/BeforeGoing/Resource/Info.plist +++ b/BeforeGoing/Resource/Info.plist @@ -2,18 +2,12 @@ - LSApplicationQueriesSchemes - - kakaokompassauth - - UIUserInterfaceStyle - Light - NSLocationAlwaysUsageDescription - 앱이 닫혀 있을 때에도 날씨 정보를 미리 업데이트하여 정확한 정보를 제공하기 위해 위치를 사용합니다. NSLocationWhenInUseUsageDescription - 현재 위치의 날씨 정보를 제공하고, 사용자가 직접 미션을 추가할 때 지리적 태그를 지정하기 위해 필요합니다. + 사용자의 현재 위치를 기반으로 기상정보와 준비물을 추천해 드리기 위해 위치 권한을 사용합니다 NSLocationAlwaysAndWhenInUseUsageDescription 앱 사용 여부와 관계없이 현재 위치의 날씨를 확인하고 정확한 알림을 제공하는 데 사용됩니다. + NSLocationAlwaysUsageDescription + 앱이 닫혀 있을 때에도 날씨 정보를 미리 업데이트하여 정확한 정보를 제공하기 위해 위치를 사용합니다. BASE_URL $(BASE_URL) CFBundleURLTypes @@ -29,6 +23,10 @@ KAKAO_NATIVE_APP_KEY $(KAKAO_NATIVE_APP_KEY) + LSApplicationQueriesSchemes + + kakaokompassauth + UIAppFonts Pretendard-SemiBold.otf @@ -56,9 +54,5 @@ - UIBackgroundModes - - location -