diff --git a/App/Sources/DesignSystem/Button/DeactivateButton/DeactivateButton.swift b/App/Sources/DesignSystem/Button/DeactivateButton/DeactivateButton.swift new file mode 100644 index 000000000..3576dd362 --- /dev/null +++ b/App/Sources/DesignSystem/Button/DeactivateButton/DeactivateButton.swift @@ -0,0 +1,32 @@ +import SwiftUI + +public struct DeactivateButton: View { + let text: String + let action: () -> Void + + public init( + text: String, + action: @escaping () -> Void = {} + ) { + self.text = text + self.action = action + } + + public var body: some View { + HStack(spacing: 10) { + BitgouelAsset.Icons.minusFill.swiftUIImage + .renderingMode(.template) + + BitgouelText( + text: text, + font: .text2 + ) + } + .foregroundColor(.white) + .padding(.vertical, 12) + .padding(.horizontal, 68) + .background(Color.bitgouel(.error(.e5))) + .cornerRadius(8, corners: .allCorners) + .buttonWrapper(action) + } +} diff --git a/App/Sources/DesignSystem/View/Row/UserInfoListRow.swift b/App/Sources/DesignSystem/View/Row/UserInfoListRow.swift index b787e0613..cdcfe3091 100644 --- a/App/Sources/DesignSystem/View/Row/UserInfoListRow.swift +++ b/App/Sources/DesignSystem/View/Row/UserInfoListRow.swift @@ -2,7 +2,7 @@ import SwiftUI public struct UserInfoListRow: View { let name: String - let authoruty: String + let authority: String let phoneNumber: String let email: String let hasCheckButton: Bool @@ -11,7 +11,7 @@ public struct UserInfoListRow: View { public init( name: String, - authoruty: String, + authority: String, phoneNumber: String, email: String, hasCheckButton: Bool = false, @@ -19,7 +19,7 @@ public struct UserInfoListRow: View { isSelected: Binding = .constant(true) ) { self.name = name - self.authoruty = authoruty + self.authority = authority self.phoneNumber = phoneNumber self.email = email self.hasCheckButton = hasCheckButton @@ -41,7 +41,7 @@ public struct UserInfoListRow: View { ) BitgouelText( - text: authoruty, + text: authority, font: .text3 ) .foregroundColor(.bitgouel(.greyscale(.g4))) diff --git a/App/Sources/DesignSystem/Button/AcceptButton/AcceptButton.swift b/App/Sources/Feature/RequestUserSignupFeature/Source/Component/Button/AcceptButton.swift similarity index 100% rename from App/Sources/DesignSystem/Button/AcceptButton/AcceptButton.swift rename to App/Sources/Feature/RequestUserSignupFeature/Source/Component/Button/AcceptButton.swift diff --git a/App/Sources/DesignSystem/Button/RejectionButton/RejectionButton.swift b/App/Sources/Feature/RequestUserSignupFeature/Source/Component/Button/RejectionButton.swift similarity index 100% rename from App/Sources/DesignSystem/Button/RejectionButton/RejectionButton.swift rename to App/Sources/Feature/RequestUserSignupFeature/Source/Component/Button/RejectionButton.swift diff --git a/App/Sources/Feature/RequestUserSignupFeature/Source/RequestUserSignupView.swift b/App/Sources/Feature/RequestUserSignupFeature/Source/RequestUserSignupView.swift index 3d452f63b..532a3476c 100644 --- a/App/Sources/Feature/RequestUserSignupFeature/Source/RequestUserSignupView.swift +++ b/App/Sources/Feature/RequestUserSignupFeature/Source/RequestUserSignupView.swift @@ -34,11 +34,10 @@ struct RequestUserSignupView: View { set: { isSelected in if isSelected { viewModel.insertAllUserList() - viewModel.updateIsSelectedUserList(isSelected: isSelected) } else { viewModel.removeAllUserList() - viewModel.updateIsSelectedUserList(isSelected: isSelected) } + viewModel.updateIsSelectedUserList(isSelected: isSelected) } ) ) @@ -57,7 +56,7 @@ struct RequestUserSignupView: View { ForEach(viewModel.userList, id: \.userID) { userInfo in UserInfoListRow( name: userInfo.name, - authoruty: userInfo.authority.display(), + authority: userInfo.authority.display(), phoneNumber: userInfo.phoneNumber, email: userInfo.email, hasCheckButton: true, diff --git a/App/Sources/Feature/UserListFeature/Source/UserListView.swift b/App/Sources/Feature/UserListFeature/Source/UserListView.swift index 96ebbf76a..9ed03a88f 100644 --- a/App/Sources/Feature/UserListFeature/Source/UserListView.swift +++ b/App/Sources/Feature/UserListFeature/Source/UserListView.swift @@ -46,7 +46,7 @@ struct UserListView: View { ForEach(viewModel.userList, id: \.userID) { userInfo in UserInfoListRow( name: userInfo.name, - authoruty: userInfo.authority.display(), + authority: userInfo.authority.display(), phoneNumber: userInfo.phoneNumber.withHypen, email: userInfo.email ) diff --git a/App/Sources/Feature/WithdrawUserListFeature/Source/Component/BottomSheet/UserCohortBottomSheet.swift b/App/Sources/Feature/WithdrawUserListFeature/Source/Component/BottomSheet/UserCohortBottomSheet.swift new file mode 100644 index 000000000..d1d004f66 --- /dev/null +++ b/App/Sources/Feature/WithdrawUserListFeature/Source/Component/BottomSheet/UserCohortBottomSheet.swift @@ -0,0 +1,74 @@ +import Service +import SwiftUI + +struct UserCohortBottomSheet: View { + let currentYear: Int + var selectedCohort: Int + let onCohortSelect: (Int) -> Void + let cancel: (Bool) -> Void + + var body: some View { + VStack(spacing: 8) { + HStack { + BitgouelText( + text: "기수", + font: .title3 + ) + + Spacer() + + Button { + cancel(false) + } label: { + BitgouelAsset.Icons.cancel.swiftUIImage + } + } + .padding(.top, 24) + + Spacer() + + ScrollView { + VStack(spacing: 16) { + ForEach(2022...currentYear, id: \.self) { cohort in + userCohortTypeRow( + cohort: cohort - 2021, + selectedCohort: selectedCohort, + onCohortSelect: onCohortSelect + ) + } + + Spacer() + } + } + } + .padding(.horizontal, 24) + } + + @ViewBuilder + func userCohortTypeRow( + cohort: Int, + selectedCohort: Int?, + onCohortSelect: @escaping (Int) -> Void + ) -> some View { + HStack { + Text("\(cohort)기") + + Spacer() + + BitgouelRadioButton( + isSelected: Binding( + get: { selectedCohort == cohort }, + set: { isSelected in + if isSelected { + onCohortSelect(cohort) + } + } + ) + ) + } + .padding(.vertical, 24) + .onTapGesture { + onCohortSelect(cohort) + } + } +} diff --git a/App/Sources/Feature/WithdrawUserListFeature/Source/Component/UserCohortFilterPopup.swift b/App/Sources/Feature/WithdrawUserListFeature/Source/Component/UserCohortFilterPopup.swift deleted file mode 100644 index aba8b2fcf..000000000 --- a/App/Sources/Feature/WithdrawUserListFeature/Source/Component/UserCohortFilterPopup.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Service -import SwiftUI - -struct UserCohortFilterPopup: View { - let currentYear: Int - var selectedCohort: Int - let onCohortSelect: (Int) -> Void - let cancel: (Bool) -> Void - - var body: some View { - RoundedRectangle(cornerRadius: 8) - .fill(Color.white) - .frame(height: 280) - .overlay { - VStack(spacing: 0) { - HStack { - BitgouelText( - text: "기수", - font: .title3 - ) - - Spacer() - - Button { - cancel(false) - } label: { - BitgouelAsset.Icons.cancel.swiftUIImage - } - } - .padding(.top, 24) - - Spacer() - - ScrollView { - VStack(spacing: 16) { - ForEach(2022...currentYear, id: \.self) { cohort in - userCohortTypeRow( - cohort: cohort - 2021, - selectedCohort: selectedCohort, - onCohortSelect: onCohortSelect - ) - } - - Spacer() - } - } - .padding(.top, 32) - } - .padding(.horizontal, 24) - } - } - - @ViewBuilder - func userCohortTypeRow( - cohort: Int, - selectedCohort: Int?, - onCohortSelect: @escaping (Int) -> Void - ) -> some View { - HStack(spacing: 8) { - BitgouelRadioButton( - isSelected: Binding( - get: { selectedCohort == cohort }, - set: { isSelected in - if isSelected { - onCohortSelect(cohort) - } - } - ) - ) - - BitgouelText( - text: "\(cohort)기", - font: .text3 - ) - - Spacer() - } - .onTapGesture { - onCohortSelect(cohort) - } - } -} diff --git a/App/Sources/Feature/WithdrawUserListFeature/Source/WithdrawUserListView.swift b/App/Sources/Feature/WithdrawUserListFeature/Source/WithdrawUserListView.swift index db4178295..04be4b234 100644 --- a/App/Sources/Feature/WithdrawUserListFeature/Source/WithdrawUserListView.swift +++ b/App/Sources/Feature/WithdrawUserListFeature/Source/WithdrawUserListView.swift @@ -19,31 +19,33 @@ struct WithdrawUserListView: View { var body: some View { ZStack { - VStack(spacing: 0) { - HStack(spacing: 10) { - optionButton( - buttonText: "선택 탈퇴", - textColor: .bitgouel(.error(.e5)), - strokeColor: .bitgouel(.error(.e5)), - backgroundColor: .bitgouel(.greyscale(.g10)) - ) { - viewModel.isShowingWithdrawAlert = true - } + VStack(spacing: 12) { + HStack { + VStack(spacing: 4) { + BitgouelText( + text: "전체", + font: .caption + ) + .foregroundColor(.bitgouel(.greyscale(.g4))) + .padding(.top, 12) - optionButton( - buttonText: "전체 탈퇴", - textColor: .bitgouel(.greyscale(.g10)), - strokeColor: .bitgouel(.error(.e5)), - backgroundColor: .bitgouel(.error(.e5)) - ) { - if viewModel.isShowingWithdrawAlert { - viewModel.isShowingWithdrawAlert = false - } else { - viewModel.isShowingWithdrawAlert = true - viewModel.insertAllUserList() - } + CheckButton( + isSelected: Binding( + get: { viewModel.isSelectedUserList }, + set: { isSelected in + if isSelected { + viewModel.insertAllUserList() + } else { + viewModel.removeAllUserList() + } + viewModel.updateIsSelectedUserList(isSelected: isSelected) + } + ) + ) } + Spacer() + HStack { BitgouelAsset.Icons.filter.swiftUIImage @@ -53,17 +55,12 @@ struct WithdrawUserListView: View { ) } .foregroundColor(.bitgouel(.greyscale(.g4))) - .padding(.horizontal, 20) - .padding(.vertical, 9) - .overlay { - RoundedRectangle(cornerRadius: 8) - .strokeBorder(Color.bitgouel(.greyscale(.g4))) - } .onTapGesture { - viewModel.isPresentedUserCohortFilter = true + viewModel.updateIsPresentedCohortBottomSheet(isPresented: true) } } - .padding(.top, 24) + + Divider() ScrollView { if viewModel.userList.isEmpty { @@ -71,24 +68,23 @@ struct WithdrawUserListView: View { } else { LazyVStack(alignment: .leading, spacing: 0) { ForEach(viewModel.userList, id: \.userID) { userInfo in - HStack(spacing: 8) { - CheckButton( - isSelected: Binding( - get: { viewModel.selectedWithdrawUserList.contains(userInfo.userID) }, - set: { isSelected in + UserInfoListRow( + name: userInfo.name, + authority: "", + phoneNumber: userInfo.phoneNumber, + email: userInfo.email, + hasCheckButton: true, + isSelected: Binding( + get: { viewModel.selectedWithdrawUserList.contains(userInfo.userID) }, + set: { isSelected in + if isSelected { viewModel.insertUserList(userID: userInfo.userID) - if !isSelected { - viewModel.removeUserList(userID: userInfo.userID) - } + } else { + viewModel.removeUserList(userID: userInfo.userID) } - ) - ) - - BitgouelText( - text: userInfo.name, - font: .text1 + } ) - } + ) Divider() .frame(height: 1) @@ -97,39 +93,17 @@ struct WithdrawUserListView: View { } } } - .padding(.top, 24) Spacer() } .padding(.horizontal, 28) - - ZStack(alignment: .center) { - if viewModel.isPresentedUserCohortFilter { - Color.black.opacity(0.4) - .edgesIgnoringSafeArea(.all) - .onTapGesture { - viewModel.updateIsPresentedCohortFilter(isPresented: false) - } - - UserCohortFilterPopup( - currentYear: viewModel.currentYear, - selectedCohort: viewModel.selectedCohort, - onCohortSelect: { cohort in - viewModel.selectedCohort = cohort - viewModel.onAppear() - }, - cancel: { cancel in - viewModel.updateIsPresentedCohortFilter(isPresented: cancel) - } - ) - .padding(.horizontal, 28) - } - } - .zIndex(1) } .onAppear { viewModel.onAppear() } + .refreshable { + viewModel.onAppear() + } .navigationTitle("탈퇴 예정자 명단") .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { @@ -152,18 +126,25 @@ struct WithdrawUserListView: View { } } } + .overlay(alignment: .bottom) { + DeactivateButton( + text: "선택한 사용자 계정 탈퇴") { + viewModel.updateIsShowingWithdrawAlert(isShowing: true) + } + } .bitgouelAlert( - title: "탈퇴를 승인 하시겠습니까?", + title: "선택한 사용자의 탈퇴를 \n승인 하시겠습니까?", description: "", isShowing: $viewModel.isShowingWithdrawAlert, alertActions: [ .init(text: "취소", style: .cancel) { - viewModel.isShowingWithdrawAlert = false - viewModel.removeAllUserList() + viewModel.updateIsShowingWithdrawAlert(isShowing: false) }, .init(text: "승인", style: .error) { - viewModel.withdrawUser() - viewModel.isShowingWithdrawAlert = false + viewModel.withdrawUser { + viewModel.updateIsShowingWithdrawAlert(isShowing: false) + viewModel.onAppear() + } } ] ) @@ -185,29 +166,18 @@ struct WithdrawUserListView: View { text: viewModel.errorMessage, isShowing: $viewModel.isErrorOccurred ) - } - - @ViewBuilder - func optionButton( - buttonText: String, - textColor: Color, - strokeColor: Color, - backgroundColor: Color, - action: @escaping () -> Void = {} - ) -> some View { - BitgouelText( - text: buttonText, - font: .text3 - ) - .foregroundColor(textColor) - .padding(.horizontal, 20) - .padding(.vertical, 9) - .overlay { - RoundedRectangle(cornerRadius: 8) - .strokeBorder(strokeColor) + .bitgouelBottomSheet(isShowing: $viewModel.isPresentedUserCohortBottomSheet) { + UserCohortBottomSheet( + currentYear: viewModel.currentYear, + selectedCohort: viewModel.selectedCohort, + onCohortSelect: { cohort in + viewModel.selectedCohort = cohort + viewModel.onAppear() + }, + cancel: { cancel in + viewModel.updateIsPresentedCohortBottomSheet(isPresented: cancel) + } + ) } - .buttonWrapper(action) - .background(backgroundColor) - .cornerRadius(8) } } diff --git a/App/Sources/Feature/WithdrawUserListFeature/Source/WithdrawUserListViewModel.swift b/App/Sources/Feature/WithdrawUserListFeature/Source/WithdrawUserListViewModel.swift index c05f56cf4..9d051e83b 100644 --- a/App/Sources/Feature/WithdrawUserListFeature/Source/WithdrawUserListViewModel.swift +++ b/App/Sources/Feature/WithdrawUserListFeature/Source/WithdrawUserListViewModel.swift @@ -4,7 +4,7 @@ import Service final class WithdrawUserListViewModel: BaseViewModel { @Published var isShowingWithdrawAlert: Bool = false @Published var isSelectedUserList = false - @Published var isPresentedUserCohortFilter: Bool = true + @Published var isPresentedUserCohortBottomSheet: Bool = true @Published var isNavigateUserListDidTap = false @Published var isNavigateRequestSignUpDidTap = false @Published var userList: [WithdrawUserInfoEntity] = [] @@ -45,14 +45,24 @@ final class WithdrawUserListViewModel: BaseViewModel { selectedWithdrawUserList.remove(userID) } - func updateIsPresentedCohortFilter(isPresented: Bool) { - isPresentedUserCohortFilter = isPresented + func updateIsPresentedCohortBottomSheet(isPresented: Bool) { + isPresentedUserCohortBottomSheet = isPresented } - func withdrawUser() { + func updateIsShowingWithdrawAlert(isShowing: Bool) { + isShowingWithdrawAlert = isShowing + } + + func updateIsSelectedUserList(isSelected: Bool) { + isSelectedUserList = isSelected + } + + func withdrawUser(_ success: @escaping () -> Void) { Task { do { try await withdrawUserUseCase(userID: selectedWithdrawUserList.joined(separator: ",")) + + success() } catch { errorMessage = error.adminDomainErrorMessage() isErrorOccurred = true @@ -66,8 +76,7 @@ final class WithdrawUserListViewModel: BaseViewModel { do { userList = try await fetchWithdrawUserListUseCase(cohort: selectedCohort.description) } catch { - errorMessage = error.adminDomainErrorMessage() - isErrorOccurred = true + print(error.localizedDescription) } } } diff --git a/Service/Sources/Domain/WithdrawDomain/DTO/Response/FetchWithdrawUserListResponseDTO.swift b/Service/Sources/Domain/WithdrawDomain/DTO/Response/FetchWithdrawUserListResponseDTO.swift index 795b44ce7..d315b4fdc 100644 --- a/Service/Sources/Domain/WithdrawDomain/DTO/Response/FetchWithdrawUserListResponseDTO.swift +++ b/Service/Sources/Domain/WithdrawDomain/DTO/Response/FetchWithdrawUserListResponseDTO.swift @@ -12,21 +12,29 @@ public struct WithdrawUserInfoResponseDTO: Decodable { public let withdrawID: Int public let userID: String public let name: String + public let email: String + public let phoneNumber: String public init( withdrawID: Int, userID: String, - name: String + name: String, + email: String, + phoneNumber: String ) { self.withdrawID = withdrawID self.userID = userID self.name = name + self.email = email + self.phoneNumber = phoneNumber } enum CodingKeys: String, CodingKey { case withdrawID = "withdrawId" case userID = "userId" - case name = "studentName" + case name + case email + case phoneNumber } } @@ -41,7 +49,9 @@ extension WithdrawUserInfoResponseDTO { WithdrawUserInfoEntity( withdrawID: withdrawID, userID: userID, - name: name + name: name, + email: email, + phoneNumber: phoneNumber ) } } diff --git a/Service/Sources/Domain/WithdrawDomain/Entity/WithdrawUserInfoEntity.swift b/Service/Sources/Domain/WithdrawDomain/Entity/WithdrawUserInfoEntity.swift index c2d010d9c..6a2b9abb2 100644 --- a/Service/Sources/Domain/WithdrawDomain/Entity/WithdrawUserInfoEntity.swift +++ b/Service/Sources/Domain/WithdrawDomain/Entity/WithdrawUserInfoEntity.swift @@ -4,14 +4,20 @@ public struct WithdrawUserInfoEntity: Equatable { public let withdrawID: Int public let userID: String public let name: String + public let email: String + public let phoneNumber: String public init( withdrawID: Int, userID: String, - name: String + name: String, + email: String, + phoneNumber: String ) { self.withdrawID = withdrawID self.userID = userID self.name = name + self.email = email + self.phoneNumber = phoneNumber } }