feat(auth): 콘텐츠 설정에서 본인인증 종료를 지원한다

This commit is contained in:
Yu Sung
2026-10-14 00:33:32 +09:00
parent ece20124d1
commit 36ac014b59
11 changed files with 528 additions and 21 deletions
+1 -1
View File
@@ -202,7 +202,7 @@ struct ChatTabView: View {
)
}
}
.authContentSettings(viewModel: mypageViewModel)
.authContentSettings(viewModel: mypageViewModel, onCancel: { pendingAction = nil })
}
}
+1 -1
View File
@@ -505,7 +505,7 @@ struct HomeTabView: View {
}
}
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
.authContentSettings(viewModel: mypageViewModel)
.authContentSettings(viewModel: mypageViewModel, onCancel: { pendingAction = nil })
}
}
+26 -6
View File
@@ -1639,20 +1639,40 @@ enum I18n {
enum Content {
static var authSensitiveContentOnDescription: String {
pick(
ko: "성인인증 완료 후 성인 콘텐츠를 볼 수 있어요",
en: "You can view adult content after age verification.",
ja: "年齢認証完了後、成人向けコンテンツを表示できます"
ko: "본인인증 완료 후 민감한 콘텐츠를 볼 수 있어요",
en: "You can view sensitive content after identity verification.",
ja: "本人認証完了後、センシティブなコンテンツを表示できます"
)
}
static var authSensitiveContentOffDescription: String {
pick(
ko: "성인 콘텐츠가 목록과 검색에 노출되지 않아요",
en: "Adult content will not appear in lists or search.",
ja: "成人向けコンテンツは一覧や検索に表示されません"
ko: "민감한 콘텐츠가 목록과 검색에 노출되지 않아요",
en: "Sensitive content will not appear in lists or search.",
ja: "センシティブなコンテンツは一覧や検索に表示されません"
)
}
static var authExitTitle: String {
pick(ko: "본인인증 종료", en: "End identity verification", ja: "本人認証の終了")
}
static var authExitMessage: String {
pick(
ko: "본인인증을 종료하시겠어요?\n종료하면 다시 본인인증을 진행해야 해요.",
en: "Do you want to end identity verification?\nIf you end it, you’ll need to verify your identity again.",
ja: "本人認証を終了しますか?\n終了すると、もう一度本人認証を行う必要があります。"
)
}
static var authExitConfirm: String {
pick(ko: "종료하기", en: "End verification", ja: "終了する")
}
static var authExitCancel: String {
pick(ko: "계속하기", en: "Continue", ja: "続ける")
}
static var preferredContent: String {
pick(ko: "선호 콘텐츠", en: "Preferred content", ja: "好みのコンテンツ")
}
@@ -151,7 +151,7 @@ struct LiveNowAllView: View {
)
}
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
.authContentSettings(viewModel: mypageViewModel)
.authContentSettings(viewModel: mypageViewModel, onCancel: { pendingAction = nil })
}
private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) {
+1 -1
View File
@@ -459,7 +459,7 @@ struct HomeView: View {
autohideIn: 2
)
}
.authContentSettings(viewModel: mypageViewModel)
.authContentSettings(viewModel: mypageViewModel, onCancel: { pendingAction = nil })
}
private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) {
@@ -119,6 +119,19 @@ final class MyPageViewModel: ObservableObject {
isShowAuthContentSettings = true
}
func cancelAuthContentSettings() {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
self?.cancelAuthContentSettings()
}
return
}
guard !isSubmittingAuth, isShowAuthContentSettings else { return }
clearPendingAuthVerification()
isShowPopup = false
}
func confirmAuthContentSettings() {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
@@ -21,9 +21,12 @@ struct AuthContentSettingsSheet: View {
@Binding var isAdultContentVisible: Bool
@Binding var contentType: ContentType
let onConfirm: () -> Void
let onClose: () -> Void
var body: some View {
VStack(spacing: 0) {
header
ViewThatFits(in: .vertical) {
settingsContent
.fixedSize(horizontal: false, vertical: true)
@@ -49,17 +52,42 @@ struct AuthContentSettingsSheet: View {
.fixedSize(horizontal: false, vertical: true)
}
.background(Color.gray900)
.accessibilityAction(.escape, onClose)
}
private var settingsContent: some View {
VStack(spacing: 0) {
private var header: some View {
HStack(spacing: 0) {
Color.clear
.frame(width: AuthContentSettingsLayout.touchHeight, height: AuthContentSettingsLayout.touchHeight)
.accessibilityHidden(true)
Text(I18n.Settings.contentViewSettings)
.appFont(size: SodaTypography.heading4.size, weight: .medium)
.foregroundColor(.white)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, minHeight: AuthContentSettingsLayout.headerHeight)
.frame(maxWidth: .infinity)
.fixedSize(horizontal: false, vertical: true)
.accessibilityAddTraits(.isHeader)
Button(action: onClose) {
Image(systemName: "xmark")
.font(.system(size: SodaTypography.body2.size, weight: .medium))
.foregroundColor(.gray400)
.frame(width: AuthContentSettingsLayout.touchHeight, height: AuthContentSettingsLayout.touchHeight)
.contentShape(Rectangle())
}
.buttonStyle(.borderless)
.accessibilityLabel(I18n.Settings.Content.authExitTitle)
.accessibilityIdentifier("authContentSettings.close")
}
.frame(minHeight: AuthContentSettingsLayout.headerHeight)
.padding(.top, AuthContentSettingsLayout.topInset)
.padding(.horizontal, SodaSpacing.s14)
.fixedSize(horizontal: false, vertical: true)
}
private var settingsContent: some View {
VStack(spacing: 0) {
sensitiveContentRow
.padding(.top, SodaSpacing.s8)
.padding(.bottom, isAdultContentVisible ? SodaSpacing.s14 : SodaSpacing.s8)
@@ -83,7 +111,6 @@ struct AuthContentSettingsSheet: View {
.padding(.top, isAdultContentVisible ? 0 : SodaSpacing.s8)
.padding(.bottom, SodaSpacing.s20)
}
.padding(.top, AuthContentSettingsLayout.topInset)
.padding(.horizontal, SodaSpacing.s14)
}
@@ -193,9 +220,12 @@ private struct AuthContentSettingsToggleStyle: ToggleStyle {
private struct AuthContentSettingsModifier: ViewModifier {
@ObservedObject var viewModel: MyPageViewModel
let onCancel: () -> Void
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var isHostPresented = false
@State private var panelProgress: CGFloat = 0
@State private var isExitDialogPresented = false
@AccessibilityFocusState private var isExitDialogFocused: Bool
private var callerErrorIsPresented: Binding<Bool> {
Binding(
@@ -214,6 +244,7 @@ private struct AuthContentSettingsModifier: ViewModifier {
}
.sodaToast(isPresented: callerErrorIsPresented, message: viewModel.errorMessage, autohideIn: 2)
.fullScreenCover(isPresented: $isHostPresented, onDismiss: {
dismissExitDialog()
panelProgress = 0
viewModel.submitPendingAuthVerification()
}) {
@@ -225,7 +256,8 @@ private struct AuthContentSettingsModifier: ViewModifier {
AuthContentSettingsSheet(
isAdultContentVisible: $viewModel.authIsAdultContentVisible,
contentType: $viewModel.authContentType,
onConfirm: viewModel.confirmAuthContentSettings
onConfirm: viewModel.confirmAuthContentSettings,
onClose: showExitDialog
)
.padding(.leading, proxy.safeAreaInsets.leading)
.padding(.trailing, proxy.safeAreaInsets.trailing)
@@ -241,6 +273,8 @@ private struct AuthContentSettingsModifier: ViewModifier {
}
}
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
.disabled(isExitDialogPresented)
.accessibilityHidden(isExitDialogPresented)
.modifier(AuthContentSettingsPanelMotion(
progress: panelProgress,
reduceMotion: reduceMotion,
@@ -249,6 +283,11 @@ private struct AuthContentSettingsModifier: ViewModifier {
}
.ignoresSafeArea(.container, edges: [.horizontal, .bottom])
}
.overlay {
if isExitDialogPresented {
exitDialog
}
}
.interactiveDismissDisabled(true)
.presentationBackground(.clear)
.onAppear {
@@ -256,6 +295,7 @@ private struct AuthContentSettingsModifier: ViewModifier {
updatePanel()
}
}
.onDisappear(perform: dismissExitDialog)
}
.onAppear {
if viewModel.isShowAuthContentSettings {
@@ -263,6 +303,9 @@ private struct AuthContentSettingsModifier: ViewModifier {
}
}
.onChange(of: viewModel.isShowAuthContentSettings) { isPresented in
if !isPresented {
dismissExitDialog()
}
if isPresented && !isHostPresented {
setHostPresented(true)
} else if isHostPresented {
@@ -276,6 +319,45 @@ private struct AuthContentSettingsModifier: ViewModifier {
}
}
private var exitDialog: some View {
SodaV2ActionModal(
title: I18n.Settings.Content.authExitTitle,
message: I18n.Settings.Content.authExitMessage,
button1: SodaV2ActionModalButton(
label: I18n.Settings.Content.authExitConfirm,
action: confirmExit
),
button2: SodaV2ActionModalButton(
label: I18n.Settings.Content.authExitCancel,
action: dismissExitDialog
),
onDimmedTap: dismissExitDialog
)
.accessibilityElement(children: .contain)
.accessibilityFocused($isExitDialogFocused)
.accessibilityAction(.escape, dismissExitDialog)
.onAppear {
isExitDialogFocused = true
}
}
private func showExitDialog() {
guard isHostPresented, viewModel.isShowAuthContentSettings, !viewModel.isSubmittingAuth else { return }
isExitDialogPresented = true
}
private func dismissExitDialog() {
isExitDialogPresented = false
isExitDialogFocused = false
}
private func confirmExit() {
guard isExitDialogPresented, isHostPresented, viewModel.isShowAuthContentSettings, !viewModel.isSubmittingAuth else { return }
dismissExitDialog()
viewModel.cancelAuthContentSettings()
onCancel()
}
private func setHostPresented(_ isPresented: Bool) {
var transaction = Transaction()
transaction.disablesAnimations = true
@@ -323,19 +405,19 @@ private struct AuthContentSettingsPanelMotion: GeometryEffect {
}
extension View {
func authContentSettings(viewModel: MyPageViewModel) -> some View {
modifier(AuthContentSettingsModifier(viewModel: viewModel))
func authContentSettings(viewModel: MyPageViewModel, onCancel: @escaping () -> Void = {}) -> some View {
modifier(AuthContentSettingsModifier(viewModel: viewModel, onCancel: onCancel))
}
}
struct AuthContentSettingsSheet_Previews: PreviewProvider {
static var previews: some View {
Group {
AuthContentSettingsSheet(isAdultContentVisible: .constant(true), contentType: .constant(.ALL), onConfirm: {})
AuthContentSettingsSheet(isAdultContentVisible: .constant(true), contentType: .constant(.ALL), onConfirm: {}, onClose: {})
.frame(width: 402)
.previewDisplayName("ON / ALL")
AuthContentSettingsSheet(isAdultContentVisible: .constant(false), contentType: .constant(.ALL), onConfirm: {})
AuthContentSettingsSheet(isAdultContentVisible: .constant(false), contentType: .constant(.ALL), onConfirm: {}, onClose: {})
.frame(width: 402)
.previewDisplayName("OFF / ALL")
}
@@ -258,7 +258,7 @@ struct CreatorChannelView: View {
.fullScreenCover(isPresented: $isShowAuthView, onDismiss: mypageViewModel.presentAuthContentSettings) {
authView
}
.authContentSettings(viewModel: mypageViewModel)
.authContentSettings(viewModel: mypageViewModel, onCancel: { pendingAction = nil })
.onAppear {
configurePayload()
if viewModel.hasLoaded == false {
+1 -1
View File
@@ -174,7 +174,7 @@ struct MainView: View {
autohideIn: 2
)
}
.authContentSettings(viewModel: mypageViewModel)
.authContentSettings(viewModel: mypageViewModel, onCancel: { pendingAction = nil })
}
private var shouldShowCreatorActionMenu: Bool {