diff --git a/GAMSS/Sources/Core/Components/Modal/ModalAction.swift b/GAMSS/Sources/Core/Components/Modal/ModalAction.swift new file mode 100644 index 0000000..29b818f --- /dev/null +++ b/GAMSS/Sources/Core/Components/Modal/ModalAction.swift @@ -0,0 +1,24 @@ +// +// ModalAction.swift +// GAMSS +// +// Created by 이건준 on 8/14/26. +// + +import Foundation + +struct ModalAction { + let title: String + let style: ModalButtonStyle + let action: () -> Void + + init( + title: String, + style: ModalButtonStyle, + action: @escaping () -> Void + ) { + self.title = title + self.style = style + self.action = action + } +} diff --git a/GAMSS/Sources/Core/Components/Modal/ModalButtonStyle.swift b/GAMSS/Sources/Core/Components/Modal/ModalButtonStyle.swift new file mode 100644 index 0000000..ece11ff --- /dev/null +++ b/GAMSS/Sources/Core/Components/Modal/ModalButtonStyle.swift @@ -0,0 +1,36 @@ +// +// ModalButtonStyle.swift +// GAMSS +// +// Created by 이건준 on 8/14/26. +// + +import SwiftUI + +enum ModalButtonStyle { + case secondary + case primary + case destructive + + var foregroundColor: Color { + switch self { + case .secondary: + return .colorGray600 + case .primary: + return .colorWhite + case .destructive: + return .colorWhite + } + } + + var backgroundColor: Color { + switch self { + case .secondary: + return .colorGray100 + case .primary: + return .colorGray950 + case .destructive: + return .colorRed + } + } +} diff --git a/GAMSS/Sources/Core/Components/Modal/ModalContainerView.swift b/GAMSS/Sources/Core/Components/Modal/ModalContainerView.swift index 420c0f3..369abb7 100644 --- a/GAMSS/Sources/Core/Components/Modal/ModalContainerView.swift +++ b/GAMSS/Sources/Core/Components/Modal/ModalContainerView.swift @@ -26,7 +26,7 @@ struct ModalContainerView: View { var body: some View { ZStack { Color.colorBlack - .opacity(0.3) + .opacity(0.7) .ignoresSafeArea() .onTapGesture { guard dismissOnBackgroundTap else { return } @@ -38,6 +38,7 @@ struct ModalContainerView: View { content() .transition(.scale.combined(with: .opacity)) + .padding(.horizontal, 28) } .animation(.easeInOut(duration: 0.25), value: isPresented) } diff --git a/GAMSS/Sources/Core/Components/Modal/ModalContentView.swift b/GAMSS/Sources/Core/Components/Modal/ModalContentView.swift new file mode 100644 index 0000000..6dc5990 --- /dev/null +++ b/GAMSS/Sources/Core/Components/Modal/ModalContentView.swift @@ -0,0 +1,87 @@ +// +// ModalContentView.swift +// GAMSS +// +// Created by 이건준 on 8/14/26. +// + +import SwiftUI + +struct ModalContentView: View { + let title: String + let subtitle: String? + let actions: [ModalAction] + + init( + title: String, + subtitle: String? = nil, + actions: [ModalAction] + ) { + self.title = title + self.subtitle = subtitle + self.actions = actions + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + titleSection + + Spacer() + .frame(height: 20) + + actionSection + } + .padding(.all, 20) + .background(Color.colorWhite) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } +} + +private extension ModalContentView { + @ViewBuilder + var titleSection: some View { + VStack(alignment: .leading, spacing: 8) { + Text(title) + .typography(.subtitle2) + .foregroundStyle(Color.colorGray950) + + if let subtitle { + Text(subtitle) + .typography(.body4Medium) + .foregroundStyle(Color.colorGray500) + } + } + } +} + + +private extension ModalContentView { + @ViewBuilder + var actionSection: some View { + switch actions.count { + case 1: + actionButton(actions[0]) + + case 2: + HStack(spacing: 8) { + actionButton(actions[0]) + actionButton(actions[1]) + } + + default: + EmptyView() + } + } + + func actionButton(_ action: ModalAction) -> some View { + Button(action: action.action) { + Text(action.title) + .typography(.title5) + .frame(maxWidth: .infinity) + .frame(height: 52) + } + .foregroundStyle(action.style.foregroundColor) + .background(action.style.backgroundColor) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } +} diff --git a/GAMSS/Sources/Presentation/Setting/Account/AccountView.swift b/GAMSS/Sources/Presentation/Setting/Account/AccountView.swift index c820c65..dea6de4 100644 --- a/GAMSS/Sources/Presentation/Setting/Account/AccountView.swift +++ b/GAMSS/Sources/Presentation/Setting/Account/AccountView.swift @@ -14,6 +14,8 @@ struct AccountView: View { @SwiftUI.Environment(LoginSession.self) private var loginSession @SwiftUI.Environment(UserManager.self) private var userManager @SwiftUI.Environment(\.dismiss) private var dismiss + @State private var isLogoutModalPresented = false + @State private var isWithdrawModalPresented = false init(title: String, viewModel: AccountViewModel) { self.title = title @@ -21,29 +23,70 @@ struct AccountView: View { } var body: some View { - VStack(alignment: .leading, spacing: 0) { - header - .padding(.horizontal, 18) - .padding(.vertical, Spacing.spacing400) - - ForEach(AccountItem.allCases) { item in - accountItem(item) + ZStack { + VStack(alignment: .leading, spacing: 0) { + header .padding(.horizontal, 18) + .padding(.vertical, Spacing.spacing400) + + ForEach(AccountItem.allCases) { item in + accountItem(item) + .padding(.horizontal, 18) + } + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(Color.colorWhite) + .toolbar(.hidden, for: .navigationBar) + .alert( + viewModel.errorMessage ?? "", + isPresented: Binding( + get: { viewModel.errorMessage != nil }, + set: { if !$0 { viewModel.errorMessage = nil } } + ) + ) { + Button("확인", role: .cancel) {} } - Spacer() - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .background(Color.colorWhite) - .toolbar(.hidden, for: .navigationBar) - .alert( - viewModel.errorMessage ?? "", - isPresented: Binding( - get: { viewModel.errorMessage != nil }, - set: { if !$0 { viewModel.errorMessage = nil } } - ) - ) { - Button("확인", role: .cancel) {} + if isLogoutModalPresented { + ModalContainerView(isPresented: $isLogoutModalPresented) { + ModalContentView(title: "로그아웃 하시겠어요?", actions: [ + .init(title: "로그아웃", style: .secondary, action: { + isLogoutModalPresented = false + + Task { + await viewModel.logout() + loginSession.value = .current + } + }), + .init(title: "마저 사용하기", style: .primary, action: { + isLogoutModalPresented = false + }) + ]) + } + } + + if isWithdrawModalPresented { + ModalContainerView(isPresented: $isWithdrawModalPresented) { + ModalContentView( + title: "정말 탈퇴하시겠어요?", + subtitle: "회원 탈퇴 시 지금까지 기록된 카드와 대화 내용은\n 영원히 삭제되며 복구되지 않아요.", + actions: [ + .init(title: "뒤로가기", style: .secondary, action: { + isWithdrawModalPresented = false + }), + .init(title: "탈퇴하기", style: .destructive, action: { + isWithdrawModalPresented = false + + Task { + await viewModel.deleteMember() + loginSession.value = .current + } + }) + ]) + } + } } } @@ -81,10 +124,7 @@ struct AccountView: View { case .logout: Button { - Task { - await viewModel.logout() - loginSession.value = .current - } + isLogoutModalPresented = true } label: { row } @@ -94,10 +134,7 @@ struct AccountView: View { row case .withdraw: Button { - Task { - await viewModel.deleteMember() - loginSession.value = .current - } + isWithdrawModalPresented = true } label: { row }