diff --git a/Projects/App/Sources/Navigation/NavigationStack.swift b/Projects/App/Sources/Navigation/NavigationStack.swift index d9e4b98..2f11e20 100644 --- a/Projects/App/Sources/Navigation/NavigationStack.swift +++ b/Projects/App/Sources/Navigation/NavigationStack.swift @@ -75,7 +75,7 @@ extension PathType { case .jobOccupation(let input): EditProfileJobView(input) case .company(let input): - EmptyView() + EditProfileCompanyView(userInfo: input) case .region(let input): EmptyView() } diff --git a/Projects/Core/NetworkKit/Sources/ProfileService/ProfileService.swift b/Projects/Core/NetworkKit/Sources/ProfileService/ProfileService.swift index d5c753c..3b017d7 100644 --- a/Projects/Core/NetworkKit/Sources/ProfileService/ProfileService.swift +++ b/Projects/Core/NetworkKit/Sources/ProfileService/ProfileService.swift @@ -66,6 +66,8 @@ extension ProfileService: ProfileServiceProtocol { .init( name: userInfo.name, jobOccupation: jobOccupation, + companyId: userInfo.profile.companyId, + allowSameCompany: userInfo.dreamPartner.allowSameCompany, locationIds: userInfo.profile.locations.map { $0.id } ) ) diff --git a/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyIntent.swift b/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyIntent.swift index c69aeb9..1ef8c0b 100644 --- a/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyIntent.swift +++ b/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyIntent.swift @@ -9,19 +9,30 @@ import Foundation import CommonKit import CoreKit +import Model +import NetworkKit +import SearchCompany //MARK: - Intent class EditProfileCompanyIntent { private weak var model: EditProfileCompanyModelActionable? private let input: DataModel + private let profileService: ProfileServiceProtocol + + internal let searchCompanyIntent: SearchCompanyIntent.Intentable // MARK: Life cycle init( model: EditProfileCompanyModelActionable, - input: DataModel + input: DataModel, + searchCompanyIntent: SearchCompanyIntent.Intentable, + profileService: ProfileServiceProtocol = ProfileService.shared ) { self.input = input self.model = model + self.searchCompanyIntent = searchCompanyIntent + self.profileService = profileService + model.setUserInfo(userInfo: input.userInfo) } } @@ -29,23 +40,61 @@ class EditProfileCompanyIntent { extension EditProfileCompanyIntent { protocol Intentable { // content - func onTapNextButton() + var searchCompanyIntent: SearchCompanyIntent.Intentable { get } + func onTapNextButton(state: SearchCompanyModel.Stateful) + func showSameCompanyPopup() // default func onAppear() func task() async } - struct DataModel {} + struct DataModel { + let userInfo: UserInfo + } } //MARK: - Intentable extension EditProfileCompanyIntent: EditProfileCompanyIntent.Intentable { // default - func onAppear() {} + func onAppear() { + searchCompanyIntent.setSameCompanyPopupHandler { [weak self] state in + self?.onTapNextButton(state: state) + } + } func task() async {} // content - func onTapNextButton() {} + func onTapNextButton( + state: SearchCompanyModel.Stateful + ) { + Task { + do { + guard let company = state.selectedCompany else { return } + model?.setLoading(status: true) + var newUserInfo = input.userInfo + newUserInfo.profile.companyId = company.id + newUserInfo.dreamPartner.allowSameCompany = state.sameCompanyMatchingAvailable + try await requestPutProfile(newUserInfo: newUserInfo) + model?.setLoading(status: false) + await popView() + } catch { + model?.setLoading(status: false) + } + } + } + + func showSameCompanyPopup() { + searchCompanyIntent.showSameCompanyPopup() + } + + func requestPutProfile(newUserInfo: UserInfo) async throws { + try await profileService.requestPutUserInfo(userInfo: newUserInfo) + } + + @MainActor + func popView() { + AppCoordinator.shared.pop() + } } diff --git a/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyModel.swift b/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyModel.swift index 7f70adf..a876652 100644 --- a/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyModel.swift +++ b/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyModel.swift @@ -9,12 +9,17 @@ import Foundation import CommonKit import CoreKit +import SearchCompany +import Model final class EditProfileCompanyModel: ObservableObject { //MARK: Stateful protocol Stateful { // content + var userInfo: UserInfo? { get } + var searchCompanyState: SearchCompanyModel.Stateful { get } + var isValidated: Bool { get } // default @@ -27,7 +32,12 @@ final class EditProfileCompanyModel: ObservableObject { //MARK: State Properties // content - @Published var isValidated: Bool = false + @Published var userInfo: UserInfo? + @Published var searchCompanyState: SearchCompanyModel.Stateful + + var isValidated: Bool { + return searchCompanyState.isValidated + } // default @Published var isLoading: Bool = false @@ -35,6 +45,10 @@ final class EditProfileCompanyModel: ObservableObject { // error @Published var showErrorView: ErrorModel? @Published var showErrorAlert: ErrorModel? + + init(searchCompanyState: SearchCompanyModel.Stateful) { + self.searchCompanyState = searchCompanyState + } } extension EditProfileCompanyModel: EditProfileCompanyModel.Stateful {} @@ -42,8 +56,7 @@ extension EditProfileCompanyModel: EditProfileCompanyModel.Stateful {} //MARK: - Actionable protocol EditProfileCompanyModelActionable: AnyObject { // content - func setValidation(value: Bool) - + func setUserInfo(userInfo: UserInfo) // default func setLoading(status: Bool) @@ -55,10 +68,9 @@ protocol EditProfileCompanyModelActionable: AnyObject { extension EditProfileCompanyModel: EditProfileCompanyModelActionable { // content - func setValidation(value: Bool) { - isValidated = value + func setUserInfo(userInfo: UserInfo) { + self.userInfo = userInfo } - // default func setLoading(status: Bool) { isLoading = status diff --git a/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyView.swift b/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyView.swift index 183e0c5..4ade9bd 100644 --- a/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyView.swift +++ b/Projects/Features/Home/Sources/Profile/EditProfile/EditProfileCompany/EditProfileCompanyView.swift @@ -10,6 +10,8 @@ import SwiftUI import CoreKit import DesignCore import CommonKit +import SearchCompany +import Model public struct EditProfileCompanyView: View { @@ -18,11 +20,25 @@ public struct EditProfileCompanyView: View { private var intent: EditProfileCompanyIntent.Intentable { container.intent } private var state: EditProfileCompanyModel.Stateful { container.model } - public init() { - let model = EditProfileCompanyModel() + @FocusState var showDropDown: Bool + var bottomSpacingHeight: CGFloat { + return showDropDown ? Device.height * 0.7 : 0 + } + + public init(userInfo: UserInfo) { + let searchCompanyState = SearchCompanyModel() + let searchCompanyIntent = SearchCompanyIntent( + model: searchCompanyState, + input: .init() + ) + + let model = EditProfileCompanyModel( + searchCompanyState: searchCompanyState + ) let intent = EditProfileCompanyIntent( model: model, - input: .init() + input: .init(userInfo: userInfo), + searchCompanyIntent: searchCompanyIntent ) let container = MVIContainer( intent: intent as EditProfileCompanyIntent.Intentable, @@ -33,16 +49,82 @@ public struct EditProfileCompanyView: View { } public var body: some View { - VStack { - Text("Hello MVI") + ZStack { + ScrollView { + ScrollViewReader { proxy in + VStack(spacing: 20) { + if let userInfo = state.userInfo { + HStack { + Text("🏒 λ‚΄ νšŒμ‚¬") + .typography(.regular_12) + Text(userInfo.profile.companyName ?? "") + .pretendard( + weight: ._600, + size: 12 + ) + } + .foregroundStyle(DesignCore.Colors.grey400) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background( + Capsule() + .fill(DesignCore.Colors.yellow50) + .stroke( + Color(hex: 0xEDE9C1), + lineWidth: 1 + ) + ) + .padding(.vertical, 20) + } + + SearchCompanyView( + state: state.searchCompanyState as! SearchCompanyModel, + intent: intent.searchCompanyIntent as! SearchCompanyIntent, + showDropDown: _showDropDown + ) + .id(0) + + Spacer() + .frame(height: bottomSpacingHeight) + .id(1) + .onChange(of: showDropDown) { + if showDropDown { + withAnimation { + proxy.scrollTo(1) + } + } + } + .foregroundStyle(.red) + } + .padding(.horizontal, 20) + .onTapGesture { + withAnimation { + showDropDown = false + } + } + } + } + CTABottomButton( + title: "λ‹€μŒ", + isActive: state.searchCompanyState.isValidated + ) { + /// νšŒμ‚¬λ₯Ό μ •ν™•ν•˜κ²Œ νŒŒμ•…ν•  수 μžˆλ‹€λ©΄ -> 같은 νšŒμ‚¬ 맀칭 νŒμ—… 보여주기 + if !state.searchCompanyState.isNoCompanyHere { + intent.showSameCompanyPopup() + } else { + /// νšŒμ‚¬ μ •ν™•ν•˜κ²Œ νŒŒμ•… λΆˆκ°€ν•˜λ‹€λ©΄ λ‹€μŒ 뷰둜 + intent.onTapNextButton(state: state.searchCompanyState) + } + } } + .navigationTitle("νšŒμ‚¬ μˆ˜μ •") .task { await intent.task() } .onAppear { intent.onAppear() } - .ignoresSafeArea(.all) + .ignoresSafeArea(.keyboard, edges: .bottom) .textureBackground() .setPopNavigation { AppCoordinator.shared.pop() @@ -53,7 +135,7 @@ public struct EditProfileCompanyView: View { #Preview { NavigationView { - EditProfileCompanyView() + EditProfileCompanyView(userInfo: .mock) } } diff --git a/Projects/Features/Home/Sources/Profile/ProfileIntent.swift b/Projects/Features/Home/Sources/Profile/ProfileIntent.swift index f722ff2..b7537e3 100644 --- a/Projects/Features/Home/Sources/Profile/ProfileIntent.swift +++ b/Projects/Features/Home/Sources/Profile/ProfileIntent.swift @@ -101,7 +101,9 @@ extension ProfileIntent: ProfileIntent.Intentable { } func fetchUserInfo(_ userInfo: UserInfo) { - model?.setUserInfo(userInfo) + DispatchQueue.main.async { + self.model?.setUserInfo(userInfo) + } } func task() async {} diff --git a/Projects/Features/Home/Sources/Profile/ProfileModel.swift b/Projects/Features/Home/Sources/Profile/ProfileModel.swift index c6c8133..81eb359 100644 --- a/Projects/Features/Home/Sources/Profile/ProfileModel.swift +++ b/Projects/Features/Home/Sources/Profile/ProfileModel.swift @@ -21,7 +21,7 @@ final class ProfileModel: ObservableObject { var isPresentedDeleteConfirmSheet: Bool { get set } var selectedWidgetType: ProfileWidget? { get } - var userInfoModel: UserInfo? { get } + var userInfoModel: UserInfo? { get set } var isValidated: Bool { get } // default diff --git a/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelView.swift b/Projects/Features/Home/Sources/ProfilePannel/ProfilePanelView.swift similarity index 62% rename from Projects/Features/Home/Sources/ProfilePannel/ProfilePannelView.swift rename to Projects/Features/Home/Sources/ProfilePannel/ProfilePanelView.swift index 8015043..4a226cf 100644 --- a/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelView.swift +++ b/Projects/Features/Home/Sources/ProfilePannel/ProfilePanelView.swift @@ -1,41 +1,20 @@ // -// ProfilePannelView.swift +// ProfilePanelView.swift // Home // -// Created by κΉ€μ§€μˆ˜ on 11/3/24. +// Created by κΉ€μ§€μˆ˜ on 11/25/24. // Copyright Β© 2024 com.weave. All rights reserved. // import SwiftUI -import UIKit -import CoreKit +import Model import DesignCore import CommonKit -import Model -public struct ProfilePannelView: View { - - @StateObject var container: MVIContainer +struct ProfilePannelView: View { - private var intent: ProfilePannelIntent.Intentable { container.intent } - private var state: ProfilePannelModel.Stateful { container.model } - - public init(name: String, profile: UserInfoProfile) { - let model = ProfilePannelModel() - let intent = ProfilePannelIntent( - model: model, - input: .init( - name: name, - profile: profile - ) - ) - let container = MVIContainer( - intent: intent as ProfilePannelIntent.Intentable, - model: model as ProfilePannelModel.Stateful, - modelChangePublisher: model.objectWillChange - ) - self._container = StateObject(wrappedValue: container) - } + let name: String + let profile: UserInfoProfile @ViewBuilder var circleDot: some View { @@ -63,11 +42,11 @@ public struct ProfilePannelView: View { .shadow(.default) VStack(spacing: 4) { - Text(state.name ?? "") + Text(name) .pretendard(weight: ._600, size: 28) .foregroundStyle(Color(hex: 0x1F1F1F)) - Text("\(state.profile?.birthYear.toString() ?? "")년생") + Text("\(profile.birthYear.toString())년생") .pretendard(weight: ._500, size: 14) .foregroundStyle(Color(hex: 0xA0A0A0)) } @@ -80,11 +59,12 @@ public struct ProfilePannelView: View { horizonIconKeyValueView( icon: DesignCore.Images.businessFill.image, key: "직ꡰ", - value: state.profile?.jobOccupation ?? "-", + value: profile.jobOccupation, textColor: Color(hex: 0x5B6654), showEditIcon: true ) { - intent.onTapEditJobOccupationIcon() + guard let userInfo = AppCoordinator.shared.userInfo else { return } + onTapEditIcon(type: .jobOccupation(userInfo)) } } @@ -95,44 +75,49 @@ public struct ProfilePannelView: View { horizonIconKeyValueView( icon: DesignCore.Images.buildingFill.image, key: "직μž₯", - value: state.profile?.companyName ?? "", + value: profile.companyName, textColor: Color(hex: 0x846470), showEditIcon: true - ) + ) { + guard let userInfo = AppCoordinator.shared.userInfo else { return } + onTapEditIcon(type: .company(userInfo)) + } } - if let profile = state.profile { - innerRoundBoxView( - fillColor: DesignCore.Colors.blue50, - strokeColor: Color(hex: 0xDFE8EF) - ) { - VStack { - horizonIconKeyValueView( - icon: DesignCore.Images.locationFill.image, - key: "ν™œλ™ 지역", - value: nil, - textColor: Color(hex: 0x606D8F), - showEditIcon: true - ) - let tagModels: [TagModel] = profile.locations - .map { - .init( - id: $0.id, - name: $0.name - ) - } - - TagListView( - tagModels: tagModels, - selectedTagModels: [] - ) { _ in } - .frame( - height: TagListCollectionView.calculateHeight( - tags: tagModels, - deviceWidth: Device.width - (76 + 36) - ) - ) + innerRoundBoxView( + fillColor: DesignCore.Colors.blue50, + strokeColor: Color(hex: 0xDFE8EF) + ) { + VStack { + horizonIconKeyValueView( + icon: DesignCore.Images.locationFill.image, + key: "ν™œλ™ 지역", + value: nil, + textColor: Color(hex: 0x606D8F), + showEditIcon: true + ) { + guard let userInfo = AppCoordinator.shared.userInfo else { return } + onTapEditIcon(type: .region(userInfo)) } + + let tagModels: [TagModel] = profile.locations + .map { + .init( + id: $0.id, + name: $0.name + ) + } + + TagListView( + tagModels: tagModels, + selectedTagModels: [] + ) { _ in } + .frame( + height: TagListCollectionView.calculateHeight( + tags: tagModels, + deviceWidth: Device.width - (76 + 36) + ) + ) } } } @@ -168,12 +153,6 @@ public struct ProfilePannelView: View { .shadow(.default) } .padding(.vertical, 30) - .task { - await intent.task() - } - .onAppear { - intent.onAppear() - } } @ViewBuilder @@ -228,13 +207,12 @@ public struct ProfilePannelView: View { .fill(fillColor) } } -} - -#Preview { - NavigationView { - ProfilePannelView( - name: "김삼일", - profile: .mock - ) + + func onTapEditIcon(type: EditProfileViewType) { + Task { + await MainActor.run { + AppCoordinator.shared.push(.editProfile(type)) + } + } } } diff --git a/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelIntent.swift b/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelIntent.swift deleted file mode 100644 index 9c79188..0000000 --- a/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelIntent.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// ProfilePannelIntent.swift -// Home -// -// Created by κΉ€μ§€μˆ˜ on 11/3/24. -// Copyright Β© 2024 com.weave. All rights reserved. -// - -import Foundation -import CommonKit -import CoreKit -import Model - -//MARK: - Intent -class ProfilePannelIntent { - private weak var model: ProfilePannelModelActionable? - private let input: DataModel - - // MARK: Life cycle - init( - model: ProfilePannelModelActionable, - input: DataModel - ) { - self.input = input - self.model = model - } -} - -//MARK: - Intentable -extension ProfilePannelIntent { - protocol Intentable { - // content - func onTapEditJobOccupationIcon() - func onTapNextButton() - - // default - func onAppear() - func task() async - } - - struct DataModel { - let name: String - let profile: UserInfoProfile - } -} - -//MARK: - Intentable -extension ProfilePannelIntent: ProfilePannelIntent.Intentable { - // default - func onAppear() { - model?.setProfile(profile: input.profile) - model?.setName(name: input.name) - } - - func task() async {} - - // content - func onTapEditJobOccupationIcon() { - Task { - await MainActor.run { - if let userInfo = AppCoordinator.shared.userInfo { - AppCoordinator.shared.push( - .editProfile( - .jobOccupation( - userInfo - ) - ) - ) - } - } - } - } - func onTapNextButton() {} -} diff --git a/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelModel.swift b/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelModel.swift deleted file mode 100644 index 8da62b2..0000000 --- a/Projects/Features/Home/Sources/ProfilePannel/ProfilePannelModel.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// ProfilePannelModel.swift -// Home -// -// Created by κΉ€μ§€μˆ˜ on 11/3/24. -// Copyright Β© 2024 com.weave. All rights reserved. -// - -import Foundation -import CommonKit -import CoreKit -import Model - -final class ProfilePannelModel: ObservableObject { - - //MARK: Stateful - protocol Stateful { - // content - var name: String? { get } - var profile: UserInfoProfile? { get } - var isValidated: Bool { get } - - // default - var isLoading: Bool { get } - - // error - var showErrorView: ErrorModel? { get } - var showErrorAlert: ErrorModel? { get } - } - - //MARK: State Properties - // content - @Published var name: String? = nil - @Published var profile: UserInfoProfile? = nil - @Published var isValidated: Bool = false - - // default - @Published var isLoading: Bool = false - - // error - @Published var showErrorView: ErrorModel? - @Published var showErrorAlert: ErrorModel? -} - -extension ProfilePannelModel: ProfilePannelModel.Stateful {} - -//MARK: - Actionable -protocol ProfilePannelModelActionable: AnyObject { - // content - func setName(name: String) - func setProfile(profile: UserInfoProfile) - func setValidation(value: Bool) - - // default - func setLoading(status: Bool) - - // error - func showErrorView(error: ErrorModel) - func showErrorAlert(error: ErrorModel) - func resetError() -} - -extension ProfilePannelModel: ProfilePannelModelActionable { - // content - func setName(name: String) { - self.name = name - } - func setProfile(profile: UserInfoProfile) { - self.profile = profile - } - 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/Model/Model/Sources/Auth/Domain/UserInfo.swift b/Projects/Model/Model/Sources/Auth/Domain/UserInfo.swift index e65bd24..f471446 100644 --- a/Projects/Model/Model/Sources/Auth/Domain/UserInfo.swift +++ b/Projects/Model/Model/Sources/Auth/Domain/UserInfo.swift @@ -9,7 +9,17 @@ import Foundation import OpenapiGenerated -public struct UserInfo { +public struct UserInfo: Equatable, Identifiable, Hashable { + public static func == (lhs: UserInfo, rhs: UserInfo) -> Bool { + if lhs.id != rhs.id { return false } + if lhs.name != rhs.name { return false } + if lhs.phone != rhs.phone { return false } + if lhs.profile != rhs.profile { return false } + if lhs.dreamPartner != rhs.dreamPartner { return false } + if lhs.profileWidgets != rhs.profileWidgets { return false } + return true + } + public let id: String? public let name: String public let phone: String @@ -54,10 +64,25 @@ public struct UserInfo { } } -public struct UserInfoProfile { +public struct UserInfoProfile: Hashable, Identifiable, Equatable { + + public static func == (lhs: UserInfoProfile, rhs: UserInfoProfile) -> Bool { + if lhs.id != rhs.id { return false } + if lhs.gender != rhs.gender { return false } + if lhs.birthYear != rhs.birthYear { return false } + if lhs.companyId != rhs.companyId { return false } + if lhs.companyName != rhs.companyName { return false } + if lhs.jobOccupation != rhs.jobOccupation { return false } + if lhs.jobOccupationRawValue != rhs.jobOccupationRawValue { return false } + if lhs.locations != rhs.locations { return false } + return true + } + + public let id: String = UUID().uuidString + public let gender: GenderType public let birthYear: Int - public let companyId: String? + public var companyId: String? public var companyName: String? public let jobOccupation: String public var jobOccupationRawValue: String @@ -113,12 +138,20 @@ public struct UserInfoProfile { } } -public struct DreamPartnerInfo { +public struct DreamPartnerInfo: Equatable, Hashable { + public static func == (lhs: DreamPartnerInfo, rhs: DreamPartnerInfo) -> Bool { + if lhs.upperBirthYear != rhs.upperBirthYear { return false } + if lhs.lowerBirthYear != rhs.lowerBirthYear { return false } + if lhs.jobOccupations != rhs.jobOccupations { return false } + if lhs.distanceType != rhs.distanceType { return false } + if lhs.allowSameCompany != rhs.allowSameCompany { return false } + return true + } public let upperBirthYear: Int? public let lowerBirthYear: Int? public let jobOccupations: [String] public let distanceType: DreamPartnerDistanceType - public let allowSameCompany: Bool? + public var allowSameCompany: Bool? public init( upperBirthYear: Int?, @@ -186,7 +219,7 @@ public struct ProfileWidget: Hashable { } } -public struct LocationModel { +public struct LocationModel: Hashable { public let id: String public let name: String