diff --git a/.gitignore b/.gitignore index ae0881c..a4e97b7 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,4 @@ Derived/ Tuist/.build Projects/Core/CoreKit/Sources/Secret.swift graph.png +OpenApiGenerator/.build diff --git a/Projects/App/Sources/Debug/AuthInfoView.swift b/Projects/App/Sources/Debug/AuthInfoView.swift index a1f08f4..5649600 100644 --- a/Projects/App/Sources/Debug/AuthInfoView.swift +++ b/Projects/App/Sources/Debug/AuthInfoView.swift @@ -155,6 +155,14 @@ struct AuthDebugInfoView: View { dismissButton: .default(Text("확인")) ) } + .toolbarRole(.navigationStack) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("로그아웃", role: .destructive) { + AppCoordinator.shared.logout() + } + } + } } // 클립보드에 복사하는 함수 diff --git a/Projects/App/Sources/Navigation/NavigationStack.swift b/Projects/App/Sources/Navigation/NavigationStack.swift index 8197b6f..4d40e20 100644 --- a/Projects/App/Sources/Navigation/NavigationStack.swift +++ b/Projects/App/Sources/Navigation/NavigationStack.swift @@ -40,6 +40,9 @@ extension PathType { case .authAgreement(let input): AuthAgreementView(input) + case .profileIntro(let input): + ProfileIntroView(input) + case .authGreeting(let input): AuthGreetingView(input) case .authProfileGender(let input): @@ -55,6 +58,8 @@ extension PathType { case .authName(let input): AuthNameInputView(input) + case .dreamPartnerIntro(let input): + DreamPartnerIntroView(input) case .dreamPartnerAgeRange(let input): DreamPartnerAgeView(input) case .dreamPartnerJobOccupation(let input): diff --git a/Projects/Core/CommonKit/Sources/AppCoordinator.swift b/Projects/Core/CommonKit/Sources/AppCoordinator.swift index a34311b..e52c7a4 100644 --- a/Projects/Core/CommonKit/Sources/AppCoordinator.swift +++ b/Projects/Core/CommonKit/Sources/AppCoordinator.swift @@ -113,4 +113,11 @@ public final class AppCoordinator: ObservableObject { } return userInfo } + + public func logout() { + TokenManager.accessToken = nil + TokenManager.refreshToken = nil + TokenManager.registerToken = nil + AuthState.change(.loggedOut) + } } diff --git a/Projects/Core/CommonKit/Sources/Path/PathTypes.swift b/Projects/Core/CommonKit/Sources/Path/PathTypes.swift index 8666e05..f1271f4 100644 --- a/Projects/Core/CommonKit/Sources/Path/PathTypes.swift +++ b/Projects/Core/CommonKit/Sources/Path/PathTypes.swift @@ -52,6 +52,7 @@ public enum PathType: Hashable { .signUp(.authRegion(input: .mock)), .signUp(.authPhoneInput), + .signUp(.dreamPartnerIntro(input: .mock)), .signUp(.dreamPartnerAgeRange(input: .mock)) ] #endif @@ -69,6 +70,8 @@ public enum PathType: Hashable { case .authPhoneVerify: return "전화번호 인증" case .authAgreement: return "이용 약관" + case .profileIntro: return "프로필 입력 인트로" + case .authGreeting: return "가입 후 환영" case .authProfileGender: return "성별 입력" case .authProfileAge: return "나이 입력" @@ -77,6 +80,7 @@ public enum PathType: Hashable { case .authRegion: return "내 지역, 선호 지역 입력" case .authName: return "이름 입력" + case .dreamPartnerIntro: return "이상형 입력 인트로" case .dreamPartnerAgeRange: return "이상형 나이대" case .dreamPartnerJobOccupation: return "이상형 직업" case .dreamPartnerDistance: return "이상형과의 거리" @@ -90,6 +94,7 @@ public enum SignUpSubViewType: Hashable { case authPhoneVerify(SMSSendResponse) case authAgreement(input: SignUpFormDomain) + case profileIntro(input: SignUpFormDomain) case authGreeting(input: SignUpFormDomain) case authProfileGender(input: SignUpFormDomain) case authProfileAge(input: SignUpFormDomain) @@ -98,6 +103,7 @@ public enum SignUpSubViewType: Hashable { case authRegion(input: SignUpFormDomain) case authName(input: SignUpFormDomain) + case dreamPartnerIntro(input: SignUpFormDomain) case dreamPartnerAgeRange(input: SignUpFormDomain) case dreamPartnerJobOccupation(input: SignUpFormDomain) case dreamPartnerDistance(input: SignUpFormDomain) @@ -108,6 +114,8 @@ public enum SignUpSubViewType: Hashable { public func hash(into hasher: inout Hasher) { switch self { + case .profileIntro: + hasher.combine(-1) case .authPhoneInput: hasher.combine(0) case .authPhoneVerify: @@ -129,6 +137,8 @@ public enum SignUpSubViewType: Hashable { case .authName: hasher.combine(9) + case .dreamPartnerIntro: + hasher.combine(-2) case .dreamPartnerAgeRange: hasher.combine(10) case .dreamPartnerJobOccupation: diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/Contents.json b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/Contents.json new file mode 100644 index 0000000..a87858b --- /dev/null +++ b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "profile_border.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "profile_border@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "profile_border@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/profile_border.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/profile_border.png new file mode 100644 index 0000000..e37dd4a Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/profile_border.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/profile_border@2x.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/profile_border@2x.png new file mode 100644 index 0000000..88593de Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/profile_border@2x.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/profile_border@3x.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/profile_border@3x.png new file mode 100644 index 0000000..f711a26 Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_border.imageset/profile_border@3x.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/Contents.json b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/Contents.json new file mode 100644 index 0000000..302f6ec --- /dev/null +++ b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "profile_default.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "profile_default@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "profile_default@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/profile_default.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/profile_default.png new file mode 100644 index 0000000..143c22e Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/profile_default.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/profile_default@2x.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/profile_default@2x.png new file mode 100644 index 0000000..05da14d Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/profile_default@2x.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/profile_default@3x.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/profile_default@3x.png new file mode 100644 index 0000000..6902bc2 Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/Profile/profile_default.imageset/profile_default@3x.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/Contents.json b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/Contents.json new file mode 100644 index 0000000..041e9b4 --- /dev/null +++ b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "heart-with-arrow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "heart-with-arrow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "heart-with-arrow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/heart-with-arrow.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/heart-with-arrow.png new file mode 100644 index 0000000..68ebd91 Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/heart-with-arrow.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/heart-with-arrow@2x.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/heart-with-arrow@2x.png new file mode 100644 index 0000000..7228a20 Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/heart-with-arrow@2x.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/heart-with-arrow@3x.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/heart-with-arrow@3x.png new file mode 100644 index 0000000..3284f6c Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/heart-with-arrow.imageset/heart-with-arrow@3x.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/Contents.json b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/Contents.json new file mode 100644 index 0000000..669c4e9 --- /dev/null +++ b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "magnifying-glass.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "magnifying-glass@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "magnifying-glass@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/magnifying-glass.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/magnifying-glass.png new file mode 100644 index 0000000..dcd7cf2 Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/magnifying-glass.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/magnifying-glass@2x.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/magnifying-glass@2x.png new file mode 100644 index 0000000..f704ece Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/magnifying-glass@2x.png differ diff --git a/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/magnifying-glass@3x.png b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/magnifying-glass@3x.png new file mode 100644 index 0000000..13872ce Binary files /dev/null and b/Projects/DesignSystem/DesignCore/Resources/Images/Images.xcassets/magnifying-glass.imageset/magnifying-glass@3x.png differ diff --git a/Projects/DesignSystem/DesignCore/Sources/Tooltip/ToolTip.swift b/Projects/DesignSystem/DesignCore/Sources/Tooltip/ToolTip.swift index 56da3fd..7da4c16 100644 --- a/Projects/DesignSystem/DesignCore/Sources/Tooltip/ToolTip.swift +++ b/Projects/DesignSystem/DesignCore/Sources/Tooltip/ToolTip.swift @@ -9,37 +9,63 @@ import SwiftUI extension View { - public func tooltip(message: String, offset: CGFloat) -> some View { + public func tooltip(message: String?, offset: CGFloat) -> some View { + return modifier(ToolTipViewModifier(message: message, offset: offset)) + } + + public func tooltip(message: AttributedString?, offset: CGFloat) -> some View { return modifier(ToolTipViewModifier(message: message, offset: offset)) } } struct ToolTipViewModifier: ViewModifier { - let message: String + let message: AttributedString? let offset: CGFloat + init( + message: AttributedString?, + offset: CGFloat + ) { + self.message = message + self.offset = offset + } + + init( + message: String?, + offset: CGFloat + ) { + if let message { + self.message = .init(stringLiteral: message) + } else { + self.message = nil + } + self.offset = offset + } + func body(content: Content) -> some View { content .overlay { - Text(message) - .padding(.horizontal, 20) - .padding(.vertical, 10) - .typography(.regular_12) - .foregroundStyle(.white) - .multilineTextAlignment(.center) - .background { - ZStack { - RoundedRectangle(cornerRadius: 6) - .fill(DesignCore.Colors.grey400) - VStack { - Spacer() - InvertedTriangle() + if let message { + Text(message) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .typography(.regular_12) + .foregroundStyle(.white) + .multilineTextAlignment(.center) + .background { + ZStack { + RoundedRectangle(cornerRadius: 6) + .fill(DesignCore.Colors.grey400) + VStack { + Spacer() + InvertedTriangle() + } + .offset(y: 12) } - .offset(y: 12) } - } - .frame(width: 300) - .offset(y: -offset) +// .frame(width: 300) + .offset(y: -offset) + } } } } diff --git a/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelView.swift b/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelView.swift index 5045ddf..0280d3a 100644 --- a/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelView.swift +++ b/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelView.swift @@ -52,8 +52,12 @@ public struct ProfilePannelView: View { VStack(spacing: 6) { HStack { Spacer() - RoundedRectangle(cornerRadius: 24) - .frame(width: 102, height: 102) + ZStack { + DesignCore.Images.profileDefault.image + .cornerRadius(20, corners: .allCorners) + DesignCore.Images.profileBorder.image + } + .frame(width: 102, height: 102) Spacer() } .shadow(.default) diff --git a/Projects/Features/Home/UnitTest/WidgetUnitTest.swift b/Projects/Features/Home/UnitTest/WidgetUnitTest.swift index 75a6631..461749c 100644 --- a/Projects/Features/Home/UnitTest/WidgetUnitTest.swift +++ b/Projects/Features/Home/UnitTest/WidgetUnitTest.swift @@ -33,10 +33,7 @@ struct WidgetUnitTest { model: writeState, input: .init( widgetType: .body, - content: nil, - successHandler: { - - } + content: nil ), service: ProfileServiceMock() ) diff --git a/Projects/Features/SignUp/Sources/AuthSignUp/AuthAgreement/AuthAgreementIntent.swift b/Projects/Features/SignUp/Sources/AuthSignUp/AuthAgreement/AuthAgreementIntent.swift index d20bc08..aaf753f 100644 --- a/Projects/Features/SignUp/Sources/AuthSignUp/AuthAgreement/AuthAgreementIntent.swift +++ b/Projects/Features/SignUp/Sources/AuthSignUp/AuthAgreement/AuthAgreementIntent.swift @@ -53,7 +53,7 @@ extension AuthAgreementIntent: AuthAgreementIntent.Intentable { func onTapNextButton() { Task { await AppCoordinator.shared.changeRootView( - .signUp(.authGreeting(input: input.input)) + .signUp(.profileIntro(input: input.input)) ) } } diff --git a/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerAge/DreamPartnerAgeView.swift b/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerAge/DreamPartnerAgeView.swift index edd5d04..fc16aad 100644 --- a/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerAge/DreamPartnerAgeView.swift +++ b/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerAge/DreamPartnerAgeView.swift @@ -137,7 +137,7 @@ public struct DreamPartnerAgeView: View { } .ignoresSafeArea(.keyboard) .textureBackground() - .setPopNavigation { + .setNavigation(showLeftBackButton: false) { AppCoordinator.shared.pop() } .setLoading(state.isLoading) diff --git a/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerIntro/DreamPartnerIntroIntent.swift b/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerIntro/DreamPartnerIntroIntent.swift new file mode 100644 index 0000000..21fd22a --- /dev/null +++ b/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerIntro/DreamPartnerIntroIntent.swift @@ -0,0 +1,66 @@ +// +// DreamPartnerIntroIntent.swift +// SignUp +// +// Created by 김지수 on 11/21/24. +// Copyright © 2024 com.weave. All rights reserved. +// + +import Foundation +import CommonKit +import CoreKit +import Model + +//MARK: - Intent +class DreamPartnerIntroIntent { + private weak var model: DreamPartnerIntroModelActionable? + private let input: DataModel + + // MARK: Life cycle + init( + model: DreamPartnerIntroModelActionable, + input: DataModel + ) { + self.input = input + self.model = model + + Task { + try? await Task.sleep(for: .milliseconds(2500)) + await pushNextView() + } + } +} + +//MARK: - Intentable +extension DreamPartnerIntroIntent { + protocol Intentable { + // content + func pushNextView() async + func onTapNextButton() + + // default + func onAppear() + func task() async + } + + struct DataModel { + let input: SignUpFormDomain + } +} + +//MARK: - Intentable +extension DreamPartnerIntroIntent: DreamPartnerIntroIntent.Intentable { + // default + func onAppear() {} + + func task() async {} + + // content + @MainActor + func pushNextView() async { + AppCoordinator.shared.changeRootView( + .signUp(.dreamPartnerAgeRange(input: input.input)) + ) + } + func onTapNextButton() {} +} diff --git a/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerIntro/DreamPartnerIntroModel.swift b/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerIntro/DreamPartnerIntroModel.swift new file mode 100644 index 0000000..0f4d0b3 --- /dev/null +++ b/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerIntro/DreamPartnerIntroModel.swift @@ -0,0 +1,78 @@ +// +// DreamPartnerIntroModel.swift +// SignUp +// +// Created by 김지수 on 11/21/24. +// Copyright © 2024 com.weave. All rights reserved. +// + +import Foundation +import CommonKit +import CoreKit + +final class DreamPartnerIntroModel: ObservableObject { + + //MARK: Stateful + protocol Stateful { + // content + var isValidated: Bool { get } + + // default + var isLoading: Bool { get } + + // error + var showErrorView: ErrorModel? { get } + var showErrorAlert: ErrorModel? { get } + } + + //MARK: State Properties + // content + @Published var isValidated: Bool = false + + // default + @Published var isLoading: Bool = false + + // error + @Published var showErrorView: ErrorModel? + @Published var showErrorAlert: ErrorModel? +} + +extension DreamPartnerIntroModel: DreamPartnerIntroModel.Stateful {} + +//MARK: - Actionable +protocol DreamPartnerIntroModelActionable: AnyObject { + // content + func setValidation(value: Bool) + + // default + func setLoading(status: Bool) + + // error + func showErrorView(error: ErrorModel) + func showErrorAlert(error: ErrorModel) + func resetError() +} + +extension DreamPartnerIntroModel: DreamPartnerIntroModelActionable { + // content + func setValidation(value: Bool) { + isValidated = value + } + + // default + func setLoading(status: Bool) { + isLoading = status + } + + // error + func showErrorView(error: ErrorModel) { + showErrorView = error + } + func showErrorAlert(error: ErrorModel) { + showErrorAlert = error + } + func resetError() { + showErrorView = nil + showErrorAlert = nil + } +} diff --git a/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerIntro/DreamPartnerIntroView.swift b/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerIntro/DreamPartnerIntroView.swift new file mode 100644 index 0000000..b5da906 --- /dev/null +++ b/Projects/Features/SignUp/Sources/DreamPartnerInput/DreamPartnerIntro/DreamPartnerIntroView.swift @@ -0,0 +1,94 @@ +// +// DreamPartnerIntroView.swift +// SignUp +// +// Created by 김지수 on 11/21/24. +// Copyright © 2024 com.weave. All rights reserved. +// + +import SwiftUI +import CoreKit +import DesignCore +import CommonKit +import Model + +public struct DreamPartnerIntroView: View { + + @StateObject var container: MVIContainer + + @State var isShowIcon: Bool = false + @State var isShowText: Bool = false + + private var intent: DreamPartnerIntroIntent.Intentable { container.intent } + private var state: DreamPartnerIntroModel.Stateful { container.model } + + public init(_ input: SignUpFormDomain) { + let model = DreamPartnerIntroModel() + let intent = DreamPartnerIntroIntent( + model: model, + input: .init(input: input) + ) + let container = MVIContainer( + intent: intent as DreamPartnerIntroIntent.Intentable, + model: model as DreamPartnerIntroModel.Stateful, + modelChangePublisher: model.objectWillChange + ) + self._container = StateObject(wrappedValue: container) + } + + public var body: some View { + ZStack { + VStack { + Spacer() + LinearGradient( + colors: [ + .init(hex: 0xF3DDE5).opacity(0.0), + .init(hex: 0xF3DDE5) + ], + startPoint: .top, + endPoint: .bottom + ) + .frame(height: Device.height * 0.5) + } + + VStack(spacing: 24) { + DesignCore.Images.heartWithArrow.image + .resizable() + .frame(width: 48, height: 48) + .opacity(isShowIcon ? 1 : 0) + .offset(y: isShowIcon ? -32 : 0) + + Text("이제 당신의 취향을 알고 싶어요!\n어떤 상대를 만나면 좋을 지 알려주세요.") + .typography(.semibold_20) + .multilineTextAlignment(.center) + .foregroundStyle(DesignCore.Colors.grey500) + .opacity(isShowText ? 1 : 0) + .offset(y: isShowText ? -32 : 0) + } + .animation(.easeInOut, value: isShowIcon) + .animation(.easeInOut, value: isShowText) + } + .ignoresSafeArea(.all) + .textureBackground() + .setPopNavigation { + AppCoordinator.shared.pop() + } + .setLoading(state.isLoading) + .task { + await intent.task() + try? await Task.sleep(for: .milliseconds(250)) + isShowIcon = true + try? await Task.sleep(for: .milliseconds(150)) + isShowText = true + } + .onAppear { + intent.onAppear() + } + } +} + +#Preview { + NavigationView { + DreamPartnerIntroView(.mock) + } +} diff --git a/Projects/Features/SignUp/Sources/ProfileInput/AuthCompany/AuthCompanyIntent.swift b/Projects/Features/SignUp/Sources/ProfileInput/AuthCompany/AuthCompanyIntent.swift index 26ab10a..f1fb2c0 100644 --- a/Projects/Features/SignUp/Sources/ProfileInput/AuthCompany/AuthCompanyIntent.swift +++ b/Projects/Features/SignUp/Sources/ProfileInput/AuthCompany/AuthCompanyIntent.swift @@ -146,6 +146,7 @@ extension AuthCompanyIntent: AuthCompanyIntent.Intentable { Task { var payload = input.input payload.profile?.companyId = state.selectedCompany?.id + payload.dreamPartner?.allowSameCompany = state.sameCompanyMatchingAvailable await pushNextView(payload: payload) } } diff --git a/Projects/Features/SignUp/Sources/ProfileInput/AuthName/AuthNameInputIntent.swift b/Projects/Features/SignUp/Sources/ProfileInput/AuthName/AuthNameInputIntent.swift index 8882716..56466d4 100644 --- a/Projects/Features/SignUp/Sources/ProfileInput/AuthName/AuthNameInputIntent.swift +++ b/Projects/Features/SignUp/Sources/ProfileInput/AuthName/AuthNameInputIntent.swift @@ -67,7 +67,7 @@ extension AuthNameInputIntent: AuthNameInputIntent.Intentable { func pushNextView(payload: SignUpFormDomain) { AppCoordinator.shared.push( .signUp( - .dreamPartnerAgeRange( + .dreamPartnerIntro( input: payload ) ) diff --git a/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputIntent.swift b/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputIntent.swift index 0019548..3fdcecf 100644 --- a/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputIntent.swift +++ b/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputIntent.swift @@ -34,6 +34,7 @@ extension AuthProfileAgeInputIntent { func onFocusChanged(_ value: Bool) func onTapNextButton(_ year: String) func onYearChanged(_ year: String) + func toggleToolTip() // default func onAppear() @@ -75,4 +76,7 @@ extension AuthProfileAgeInputIntent: AuthProfileAgeInputIntent.Intentable { ) } } + func toggleToolTip() { + model?.toggleToolTip() + } } diff --git a/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputModel.swift b/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputModel.swift index a8b6d41..dd60e19 100644 --- a/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputModel.swift +++ b/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputModel.swift @@ -21,6 +21,7 @@ final class AuthProfileAgeInputModel: ObservableObject { var isFocused: Bool { get } var isValidated: Bool { get } var targetGender: GenderType { get } + var isShowToolTip: Bool { get } // default var isLoading: Bool { get } @@ -37,6 +38,7 @@ final class AuthProfileAgeInputModel: ObservableObject { @Published var birthYear = String() @Published var isFocused: Bool = false @Published var targetGender: GenderType = .female + @Published var isShowToolTip: Bool = false // default @Published var isLoading: Bool = false @@ -55,6 +57,7 @@ protocol AuthProfileAgeInputModelActionable: AnyObject { func setFocuse(_ value: Bool) func setValidation(value: Bool) func setTargetGender(_ gender: GenderType) + func toggleToolTip() // default func setLoading(status: Bool) @@ -79,6 +82,9 @@ extension AuthProfileAgeInputModel: AuthProfileAgeInputModelActionable { func setTargetGender(_ gender: GenderType) { targetGender = gender } + func toggleToolTip() { + isShowToolTip.toggle() + } // default func setLoading(status: Bool) { isLoading = status diff --git a/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputView.swift b/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputView.swift index 94ea67f..af83b75 100644 --- a/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputView.swift +++ b/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileAge/AuthProfileAgeInputView.swift @@ -37,12 +37,32 @@ public struct AuthProfileAgeInputView: View { self._container = StateObject(wrappedValue: container) } + private var tooltipMessage: AttributedString { + let text = """ + 2024년 기준으로 2004년생(만 20살)부터 + 1989년생(만 35살)까지 가입할 수 있어요 + """ + + var attributedString = AttributedString(text) + let boldRanges = [ + text.range(of: "2004년생(만 20살)부터"), + text.range(of: "1989년생(만 35살)") + ].compactMap { $0 } + + boldRanges.forEach { range in + let attributedRange = attributedString.range(of: text[range])! + attributedString[attributedRange].inlinePresentationIntent = .stronglyEmphasized + } + + return attributedString + } + public var body: some View { VStack { ProfileInputTemplatedView( currentPage: 2, maxPage: 5, - subMessage: "좋은 \(state.targetGender) 소개시켜 드릴께요!", + subMessage: "좋은 \(state.targetGender.name)분 소개시켜 드릴께요!", mainMessage: "당신의 나이는 무엇인가요?" ) { VStack { @@ -82,7 +102,9 @@ public struct AuthProfileAgeInputView: View { .frame(height: 92) Button(action: { - + withAnimation { + intent.toggleToolTip() + } }, label: { HStack(spacing: 4) { DesignCore.Images.iconInformation.image @@ -94,6 +116,7 @@ public struct AuthProfileAgeInputView: View { .padding(.top, 20) } .padding(.top, 8) + .tooltip(message: state.isShowToolTip ? tooltipMessage : nil, offset: 0) } Spacer() @@ -114,10 +137,15 @@ public struct AuthProfileAgeInputView: View { .ignoresSafeArea() .padding(.top, 10) .textureBackground() - .setNavigation(showLeftBackButton: false) { - + .setPopNavigation { + AppCoordinator.shared.pop() } .setLoading(state.isLoading) + .onTapGesture { + if state.isShowToolTip { + intent.toggleToolTip() + } + } } } diff --git a/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileGenderInput/AuthProfileGenderInputView.swift b/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileGenderInput/AuthProfileGenderInputView.swift index 670d8b0..07ec8c7 100644 --- a/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileGenderInput/AuthProfileGenderInputView.swift +++ b/Projects/Features/SignUp/Sources/ProfileInput/AuthProfileGenderInput/AuthProfileGenderInputView.swift @@ -83,7 +83,7 @@ public struct AuthProfileGenderInputView: View { } .padding(.top, 10) .textureBackground() - .setPopNavigation { + .setNavigation(showLeftBackButton: false) { AppCoordinator.shared.pop() } .setLoading(state.isLoading) diff --git a/Projects/Features/SignUp/Sources/ProfileInput/ProfileIntro/ProfileIntroIntent.swift b/Projects/Features/SignUp/Sources/ProfileInput/ProfileIntro/ProfileIntroIntent.swift new file mode 100644 index 0000000..c989409 --- /dev/null +++ b/Projects/Features/SignUp/Sources/ProfileInput/ProfileIntro/ProfileIntroIntent.swift @@ -0,0 +1,66 @@ +// +// ProfileIntroIntent.swift +// SignUp +// +// Created by 김지수 on 11/20/24. +// Copyright © 2024 com.weave. All rights reserved. +// + +import Foundation +import CommonKit +import CoreKit +import Model + +//MARK: - Intent +class ProfileIntroIntent { + private weak var model: ProfileIntroModelActionable? + private let input: DataModel + + // MARK: Life cycle + init( + model: ProfileIntroModelActionable, + input: DataModel + ) { + self.input = input + self.model = model + + Task { + try? await Task.sleep(for: .milliseconds(2500)) + await pushNextView() + } + } +} + +//MARK: - Intentable +extension ProfileIntroIntent { + protocol Intentable { + // content + func pushNextView() async + func onTapNextButton() + + // default + func onAppear() + func task() async + } + + struct DataModel { + let input: SignUpFormDomain + } +} + +//MARK: - Intentable +extension ProfileIntroIntent: ProfileIntroIntent.Intentable { + // default + @MainActor + func pushNextView() async { + AppCoordinator.shared.changeRootView( + .signUp(.authProfileGender(input: input.input)) + ) + } + func onAppear() {} + + func task() async {} + + // content + func onTapNextButton() {} +} diff --git a/Projects/Features/SignUp/Sources/ProfileInput/ProfileIntro/ProfileIntroModel.swift b/Projects/Features/SignUp/Sources/ProfileInput/ProfileIntro/ProfileIntroModel.swift new file mode 100644 index 0000000..0d07458 --- /dev/null +++ b/Projects/Features/SignUp/Sources/ProfileInput/ProfileIntro/ProfileIntroModel.swift @@ -0,0 +1,78 @@ +// +// ProfileIntroModel.swift +// SignUp +// +// Created by 김지수 on 11/20/24. +// Copyright © 2024 com.weave. All rights reserved. +// + +import Foundation +import CommonKit +import CoreKit + +final class ProfileIntroModel: ObservableObject { + + //MARK: Stateful + protocol Stateful { + // content + var isValidated: Bool { get } + + // default + var isLoading: Bool { get } + + // error + var showErrorView: ErrorModel? { get } + var showErrorAlert: ErrorModel? { get } + } + + //MARK: State Properties + // content + @Published var isValidated: Bool = false + + // default + @Published var isLoading: Bool = false + + // error + @Published var showErrorView: ErrorModel? + @Published var showErrorAlert: ErrorModel? +} + +extension ProfileIntroModel: ProfileIntroModel.Stateful {} + +//MARK: - Actionable +protocol ProfileIntroModelActionable: AnyObject { + // content + func setValidation(value: Bool) + + // default + func setLoading(status: Bool) + + // error + func showErrorView(error: ErrorModel) + func showErrorAlert(error: ErrorModel) + func resetError() +} + +extension ProfileIntroModel: ProfileIntroModelActionable { + // content + func setValidation(value: Bool) { + isValidated = value + } + + // default + func setLoading(status: Bool) { + isLoading = status + } + + // error + func showErrorView(error: ErrorModel) { + showErrorView = error + } + func showErrorAlert(error: ErrorModel) { + showErrorAlert = error + } + func resetError() { + showErrorView = nil + showErrorAlert = nil + } +} diff --git a/Projects/Features/SignUp/Sources/ProfileInput/ProfileIntro/ProfileIntroView.swift b/Projects/Features/SignUp/Sources/ProfileInput/ProfileIntro/ProfileIntroView.swift new file mode 100644 index 0000000..a1ce01e --- /dev/null +++ b/Projects/Features/SignUp/Sources/ProfileInput/ProfileIntro/ProfileIntroView.swift @@ -0,0 +1,94 @@ +// +// ProfileIntroView.swift +// SignUp +// +// Created by 김지수 on 11/20/24. +// Copyright © 2024 com.weave. All rights reserved. +// + +import SwiftUI +import CoreKit +import DesignCore +import CommonKit +import Model + +public struct ProfileIntroView: View { + + @StateObject var container: MVIContainer + + @State var isShowIcon: Bool = false + @State var isShowText: Bool = false + + private var intent: ProfileIntroIntent.Intentable { container.intent } + private var state: ProfileIntroModel.Stateful { container.model } + + public init(_ input: SignUpFormDomain) { + let model = ProfileIntroModel() + let intent = ProfileIntroIntent( + model: model, + input: .init(input: input) + ) + let container = MVIContainer( + intent: intent as ProfileIntroIntent.Intentable, + model: model as ProfileIntroModel.Stateful, + modelChangePublisher: model.objectWillChange + ) + self._container = StateObject(wrappedValue: container) + } + + public var body: some View { + ZStack { + VStack { + Spacer() + LinearGradient( + colors: [ + .init(hex: 0xF3DDE5).opacity(0.0), + .init(hex: 0xF3DDE5) + ], + startPoint: .top, + endPoint: .bottom + ) + .frame(height: Device.height * 0.5) + } + + VStack(spacing: 24) { + DesignCore.Images.magnifyingGlass.image + .resizable() + .frame(width: 48, height: 48) + .opacity(isShowIcon ? 1 : 0) + .offset(y: isShowIcon ? -32 : 0) + + Text("만나서 반가워요!\n이제 당신이 어떤 사람인지 알려주세요.") + .typography(.semibold_20) + .multilineTextAlignment(.center) + .foregroundStyle(DesignCore.Colors.grey500) + .opacity(isShowText ? 1 : 0) + .offset(y: isShowText ? -32 : 0) + } + .animation(.easeInOut, value: isShowIcon) + .animation(.easeInOut, value: isShowText) + } + .ignoresSafeArea(.all) + .textureBackground() + .setPopNavigation { + AppCoordinator.shared.pop() + } + .setLoading(state.isLoading) + .task { + await intent.task() + try? await Task.sleep(for: .milliseconds(250)) + isShowIcon = true + try? await Task.sleep(for: .milliseconds(150)) + isShowText = true + } + .onAppear { + intent.onAppear() + } + } +} + +#Preview { + NavigationView { + ProfileIntroView(.mock) + } +} diff --git a/Projects/Model/Model/Sources/SignUp/Domain/SignUpFormDomain.swift b/Projects/Model/Model/Sources/SignUp/Domain/SignUpFormDomain.swift index 8e8b645..3898ce0 100644 --- a/Projects/Model/Model/Sources/SignUp/Domain/SignUpFormDomain.swift +++ b/Projects/Model/Model/Sources/SignUp/Domain/SignUpFormDomain.swift @@ -94,6 +94,7 @@ public struct SignUpDreamPartnerDomain { public var lowerBirthYearGap: Int? public var upperBirthYearGap: Int? public var jobOccupations: [String] + public var allowSameCompany: Bool? public var distanceType: DreamPartnerDistanceType? var toDto: Components.Schemas.UserDesiredPartner? { @@ -110,7 +111,8 @@ public struct SignUpDreamPartnerDomain { end: upperBirthYearGap ), jobOccupations: jobOccupations, - preferDistance: distanceType.toDto + preferDistance: distanceType.toDto, + allowSameCompany: allowSameCompany ) } @@ -118,12 +120,14 @@ public struct SignUpDreamPartnerDomain { lowerBirthYearGap: Int? = nil, upperBirthYearGap: Int? = nil, jobOccupations: [String], - distanceType: DreamPartnerDistanceType? = nil + distanceType: DreamPartnerDistanceType? = nil, + allowSameCompany: Bool? = nil ) { self.lowerBirthYearGap = lowerBirthYearGap self.upperBirthYearGap = upperBirthYearGap self.jobOccupations = jobOccupations self.distanceType = distanceType + self.allowSameCompany = allowSameCompany } } @@ -153,10 +157,17 @@ public enum GenderType: String, CaseIterable { case male case female - var toDto: Components.Schemas.Gender { + public var toDto: Components.Schemas.Gender { switch self { case .male: .MALE case .female: .FEMALE } } + + public var name: String { + switch self { + case .male: "남성" + case .female: "여성" + } + } }