From 124cf96a6e98c01a2752c6ed7c86c071850ff51e Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 25 Sep 2026 19:22:23 +0900 Subject: [PATCH] =?UTF-8?q?ui:=20=EA=B3=84=EC=A0=95=20=EC=97=B0=EB=8F=99?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EC=B9=B4=EB=93=9C=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resources/Localizable.xcstrings | 51 ++++ .../Sources/Settings/AccountView.swift | 228 +++++++++++++----- 2 files changed, 224 insertions(+), 55 deletions(-) diff --git a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings index b5d2e8bf..9c3e093d 100644 --- a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings +++ b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings @@ -162,6 +162,23 @@ } } }, + "account_connected" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connected" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "연결됨" + } + } + } + }, "account_current_section" : { "extractionState" : "manual", "localizations" : { @@ -196,6 +213,40 @@ } } }, + "account_not_connected" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Not Connected" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "연결되지 않음" + } + } + } + }, + "account_signed_in" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signed In" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "로그인 중" + } + } + } + }, "account_social_section" : { "extractionState" : "manual", "localizations" : { diff --git a/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift b/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift index a665c049..feb2c78b 100644 --- a/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift @@ -11,60 +11,76 @@ import PresentationShared import Domain struct AccountView: View { + @Environment(\.dismiss) private var dismiss @State var store: StoreOf var body: some View { - List { - Section(String(localized: "account_current_section", bundle: PresentationResources.bundle)) { - HStack { - if let provider = store.currentProvider { - providerContent(provider) + ScrollView { + LazyVStack(alignment: .leading, spacing: 24) { + if let currentProvider = store.currentProvider { + VStack(alignment: .leading, spacing: 12) { + Text("account_current_section", bundle: PresentationResources.bundle) + .font(.title3.weight(.semibold)) + + HStack(spacing: 12) { + ProviderIdentity( + provider: currentProvider, + status: nil + ) + + Spacer(minLength: 8) + + Text("account_signed_in", bundle: PresentationResources.bundle) + .font(.caption.weight(.semibold)) + .foregroundStyle(Color.accent) + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background(Color.accent.opacity(0.1), in: .capsule) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.surface, in: .rect(cornerRadius: 16)) } } - } - Section(String(localized: "account_social_section", bundle: PresentationResources.bundle)) { + let providers = AuthProvider.allCases.filter { $0 != store.currentProvider } - ForEach(providers, id: \.self) { provider in - let isConnected = store.connectedProviders.contains(provider) - let showProgressView = store.isLoading && store.activeLoadingProvider == provider - HStack { - providerContent(provider) - Spacer() - Button { - if isConnected { - store.send(.unlinkFromProvider(provider)) - } else { - store.send(.linkWithProvider(provider)) - } - } label: { - Text(isConnected - ? String(localized: "account_disconnect", bundle: PresentationResources.bundle) - : String(localized: "account_connect", bundle: PresentationResources.bundle)) - .font(.caption.weight(.semibold)) - .foregroundStyle(.white) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(isConnected ? Color.red : .blue) - .clipShape(.capsule) - } - .buttonStyle(.plain) - .disabled( - store.isLoading || - (!isConnected && store.presentationContext == nil) - ) - .opacity(showProgressView ? 0 : 1) - .overlay(alignment: .trailing) { - if showProgressView { - ProgressView() + VStack(alignment: .leading, spacing: 12) { + Text("account_social_section", bundle: PresentationResources.bundle) + .font(.title3.weight(.semibold)) + + VStack(spacing: 0) { + ForEach(providers, id: \.self) { provider in + let isConnected = store.connectedProviders.contains(provider) + ProviderRow( + provider: provider, + isConnected: isConnected, + showsProgress: store.isLoading && store.activeLoadingProvider == provider, + isDisabled: store.isLoading || (!isConnected && store.presentationContext == nil), + action: { + if isConnected { + store.send(.unlinkFromProvider(provider)) + } else { + store.send(.linkWithProvider(provider)) + } + } + ) + + if provider != providers.last { + Divider() + .padding(.leading, 64) + .padding(.trailing, 16) } } } + .background(Color.surface, in: .rect(cornerRadius: 16)) } } + .padding(.horizontal, 16) + .padding(.bottom, 24) } - .scrollDisabled(true) - .listStyle(.insetGrouped) - .navigationTitle(String(localized: "nav_account", bundle: PresentationResources.bundle)) + .safeAreaInset(edge: .top, spacing: 0) { topBar } + .background(Color.appBackground) + .toolbarVisibility(.hidden, for: .navigationBar) .onAppear { store.send(.onAppear) } .prominentAlert(store, state: \.alert, action: \.alert) .background { @@ -75,20 +91,122 @@ struct AccountView: View { } } } - - private func formattedProviderName(_ provider: AuthProvider) -> String { - let rawValue = provider.rawValue - let providerPrefix = rawValue.prefix(1).uppercased() - let providerSuffix = rawValue.dropFirst().prefix(while: { $0 != "." }) - return providerPrefix + providerSuffix + + private var topBar: some View { + ZStack { + Text(String(localized: "nav_account", bundle: PresentationResources.bundle)) + .font(.headline) + + HStack { + NavigationBackButton(action: { dismiss() }) + Spacer() + } + } + .padding(.horizontal, 16) + .padding(.bottom, 12) + .background(Color.appBackground) + .toolbarBackground(Color.appBackground) + } +} + +private struct ProviderRow: View { + let provider: AuthProvider + let isConnected: Bool + let showsProgress: Bool + let isDisabled: Bool + let action: () -> Void + + var body: some View { + HStack(spacing: 12) { + ProviderIdentity( + provider: provider, + status: String( + localized: isConnected ? "account_connected" : "account_not_connected", + bundle: PresentationResources.bundle + ) + ) + + Spacer(minLength: 8) + + Button(action: action) { + ZStack { + Text(isConnected + ? String(localized: "account_disconnect", bundle: PresentationResources.bundle) + : String(localized: "account_connect", bundle: PresentationResources.bundle)) + .opacity(showsProgress ? 0 : 1) + + if showsProgress { + ProgressView() + .tint(isConnected ? Color.danger : Color.white) + } + } + .font(.caption.weight(.semibold)) + .foregroundStyle(isConnected ? Color.danger : Color.white) + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background(isConnected ? Color.clear : Color.accent, in: .capsule) + .overlay { + Capsule() + .stroke(isConnected ? Color.danger : Color.clear) + } + } + .buttonStyle(.plain) + .disabled(isDisabled) + .opacity(isDisabled && !showsProgress ? 0.5 : 1) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +private struct ProviderIdentity: View { + let provider: AuthProvider + let status: String? + + var body: some View { + HStack(spacing: 12) { + Image(provider.imageName, bundle: PresentationResources.bundle) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .frame(width: 36, height: 36) + .background(Color.surfaceSecondary, in: Circle()) + + VStack(alignment: .leading, spacing: 2) { + Text(provider.displayName) + .font(.headline) + .foregroundStyle(Color.primary) + + if let status { + Text(status) + .font(.footnote) + .foregroundStyle(Color.textSecondary) + } + } + } } +} - @ViewBuilder - private func providerContent(_ provider: AuthProvider) -> some View { - Image(formattedProviderName(provider)) - .resizable() - .scaledToFit() - .frame(width: UIFont.labelFontSize) - Text(formattedProviderName(provider)) +private extension AuthProvider { + var displayName: String { + switch self { + case .apple: + return "Apple" + case .google: + return "Google" + case .github: + return "GitHub" + } + } + + var imageName: String { + switch self { + case .apple: + return "Apple" + case .google: + return "Google" + case .github: + return "Github" + } } }