feat(auth): 인증 후 콘텐츠 설정과 개발용 모의 인증을 추가한다

This commit is contained in:
Yu Sung
2026-10-13 11:37:03 +09:00
parent e34a51b411
commit 61bfa46c11
20 changed files with 1203 additions and 181 deletions
@@ -13,8 +13,18 @@ import Moya
final class AuthRepository {
private let api = MoyaProvider<AuthApi>()
func authCertificate(receiptId: String) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.auth(request: AuthVerifyRequest(receiptId: receiptId)))
func authCertificate(request: AuthVerifyRequest) -> AnyPublisher<Response, MoyaError> {
#if AUTH_VERIFICATION_MOCK
let requestToken = UserDefaults.string(forKey: .token)
guard let data = AuthVerificationMock.shared.confirm(request: request, requestToken: requestToken) else {
return Fail(error: MoyaError.statusCode(Response(statusCode: 401, data: Data())))
.eraseToAnyPublisher()
}
return Just(Response(statusCode: 200, data: data))
.setFailureType(to: MoyaError.self)
.eraseToAnyPublisher()
#else
return api.requestPublisher(.auth(request: request))
#endif
}
}
@@ -9,8 +9,12 @@ import Foundation
struct AuthVerifyRequest: Encodable {
let receiptId: String
let isAdultContentVisible: Bool
let contentType: ContentType
enum CodingKeys : String, CodingKey {
case receiptId = "receipt_id"
case isAdultContentVisible
case contentType
}
}
+12 -18
View File
@@ -8,7 +8,6 @@
import SwiftUI
import Bootpay
import BootpayUI
import Kingfisher
import RefreshableScrollView
@@ -33,28 +32,23 @@ struct MyPageView: View {
BaseView(isLoading: $viewModel.isLoading) {
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
viewModel.isShowAuthView {
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
.onConfirm {
DEBUG_LOG("onConfirm: \($0)")
return true
}
.onCancel {
DEBUG_LOG("onCancel: \($0)")
}
.onError {
DEBUG_LOG("onError: \($0)")
AuthVerificationView(
payload: payload,
onCancel: {},
onError: {
viewModel.errorMessage = I18n.Main.Auth.authenticationError
viewModel.isShowPopup = true
viewModel.isShowAuthView = false
}
.onDone {
DEBUG_LOG("onDone: \($0)")
},
onDone: {
viewModel.authVerify($0)
}
.onClose {
DEBUG_LOG("onClose")
viewModel.isShowAuthView = false
viewModel.presentAuthContentSettings()
},
onClose: {
viewModel.isShowAuthView = false
}
)
} else {
VStack(spacing: 0) {
// Header
@@ -180,7 +174,7 @@ struct MyPageView: View {
}
}
}
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
.authContentSettings(viewModel: viewModel)
.onAppear {
payload.applicationId = BOOTPAY_APP_ID
payload.price = 0
+168 -52
View File
@@ -14,6 +14,9 @@ final class MyPageViewModel: ObservableObject {
private let authRepository = AuthRepository()
private let noticeRepository = NoticeRepository()
private var subscription = Set<AnyCancellable>()
private var pendingAuthReceiptId: String?
private var pendingAuthRequest: AuthVerifyRequest?
private var pendingAuthSuccess: (() -> Void)?
@Published var latestNotice: NoticeItem? = nil
@@ -28,6 +31,16 @@ final class MyPageViewModel: ObservableObject {
@Published var isShowPopup = false
@Published var isLoading = false
@Published var isShowAuthView = false
@Published var isShowAuthContentSettings = false
@Published var authIsAdultContentVisible = true {
didSet {
if !authIsAdultContentVisible {
authContentType = .ALL
}
}
}
@Published var authContentType: ContentType = .ALL
@Published private(set) var isSubmittingAuth = false
func getMypage() {
isLoading = true
@@ -73,61 +86,164 @@ final class MyPageViewModel: ObservableObject {
}
func authVerify(_ data: [String: Any], onSuccess: (() -> Void)? = nil) {
isLoading = true
let _data = data["data"] as? [String: Any]
if let data = _data {
let receiptId = data["receipt_id"] as! String
authRepository.authCertificate(receiptId: receiptId)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<AuthResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
if data.gender == 0 {
UserDefaults.set(ContentType.FEMALE.rawValue, forKey: .contentPreference)
} else {
UserDefaults.set(ContentType.MALE.rawValue, forKey: .contentPreference)
}
if let onSuccess = onSuccess {
onSuccess()
} else {
AppState.shared.isRestartApp = true
AppState.shared.setAppStep(step: .splash)
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = I18n.MyPage.Auth.verificationErrorWithSupport
}
self.isShowPopup = true
}
} catch {
self.errorMessage = I18n.MyPage.Auth.verificationErrorWithSupport
self.isShowPopup = true
}
}
.store(in: &subscription)
} else {
isLoading = false
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
self?.authVerify(data, onSuccess: onSuccess)
}
return
}
guard pendingAuthReceiptId == nil, !isSubmittingAuth else { return }
guard let payload = data["data"] as? [String: Any],
let receiptId = payload["receipt_id"] as? String,
!receiptId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
errorMessage = I18n.Main.Auth.authenticationError
isShowPopup = true
return
}
pendingAuthReceiptId = receiptId
pendingAuthSuccess = onSuccess
authIsAdultContentVisible = true
authContentType = .ALL
}
func presentAuthContentSettings() {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
self?.presentAuthContentSettings()
}
return
}
guard pendingAuthReceiptId != nil, !isSubmittingAuth else { return }
isShowAuthContentSettings = true
}
func confirmAuthContentSettings() {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
self?.confirmAuthContentSettings()
}
return
}
guard let receiptId = pendingAuthReceiptId, !isSubmittingAuth else { return }
pendingAuthRequest = AuthVerifyRequest(
receiptId: receiptId,
isAdultContentVisible: authIsAdultContentVisible,
contentType: authIsAdultContentVisible ? authContentType : .ALL
)
isSubmittingAuth = true
isShowAuthContentSettings = false
}
func submitPendingAuthVerification() {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
self?.submitPendingAuthVerification()
}
return
}
guard isSubmittingAuth, let request = pendingAuthRequest else { return }
pendingAuthRequest = nil
authRepository.authCertificate(request: request)
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
if case .failure = result {
self?.handleAuthSubmissionFailure()
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
guard (200..<300).contains(response.statusCode) else {
self.handleAuthSubmissionFailure()
return
}
do {
let decoded = try JSONDecoder().decode(ApiResponse<AuthResponse>.self, from: response.data)
guard decoded.success, decoded.data != nil else {
self.handleAuthSubmissionFailure()
return
}
if self.pendingAuthSuccess != nil {
self.synchronizeAuthenticatedMember()
} else {
UserDefaults.set(request.isAdultContentVisible, forKey: .isAdultContentVisible)
UserDefaults.set(request.contentType.rawValue, forKey: .contentPreference)
self.restartAfterAuthVerification()
}
} catch {
self.handleAuthSubmissionFailure()
}
}
.store(in: &subscription)
}
private func handleAuthSubmissionFailure() {
pendingAuthRequest = nil
isSubmittingAuth = false
errorMessage = I18n.MyPage.Auth.verificationErrorWithSupport
isShowPopup = true
isShowAuthContentSettings = true
}
private func synchronizeAuthenticatedMember() {
repository.getMemberInfo()
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
if case .failure = result {
self?.restartAfterAuthVerification()
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
guard (200..<300).contains(response.statusCode) else {
self.restartAfterAuthVerification()
return
}
do {
let decoded = try JSONDecoder().decode(ApiResponse<GetMemberInfoResponse>.self, from: response.data)
guard let data = decoded.data, decoded.success, data.isAuth else {
self.restartAfterAuthVerification()
return
}
UserDefaults.set(data.can, forKey: .can)
UserDefaults.set(data.point, forKey: .point)
UserDefaults.set(data.isAuth, forKey: .auth)
UserDefaults.set(data.role.rawValue, forKey: .role)
UserDefaults.set(data.auditionNotice ?? false, forKey: .isAuditionNotification)
UserDefaults.set(data.countryCode, forKey: .countryCode)
UserDefaults.set(data.isAdultContentVisible, forKey: .isAdultContentVisible)
UserDefaults.set(data.isAdultContentVisible ? data.contentType : ContentType.ALL.rawValue, forKey: .contentPreference)
self.isAuth = data.isAuth
let onSuccess = self.pendingAuthSuccess
self.clearPendingAuthVerification()
onSuccess?()
} catch {
self.restartAfterAuthVerification()
}
}
.store(in: &subscription)
}
private func clearPendingAuthVerification() {
pendingAuthReceiptId = nil
pendingAuthRequest = nil
pendingAuthSuccess = nil
isSubmittingAuth = false
isShowAuthContentSettings = false
}
private func restartAfterAuthVerification() {
clearPendingAuthVerification()
AppState.shared.isRestartApp = true
AppState.shared.setAppStep(step: .splash)
}
func getLatestNotice() {