// // MyPageView.swift // SodaLive // // Created by klaus on 2023/08/09. // import SwiftUI import Bootpay import BootpayUI import PopupView import Kingfisher import RefreshableScrollView struct MyPageView: View { @StateObject var viewModel = MyPageViewModel() @StateObject var recentContentViewModel = RecentContentViewModel() @State private var payload = Payload() @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token) var body: some 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)") viewModel.errorMessage = "본인인증 중 오류가 발생했습니다." viewModel.isShowPopup = true viewModel.isShowAuthView = false } .onDone { DEBUG_LOG("onDone: \($0)") viewModel.authVerify($0) } .onClose { DEBUG_LOG("onClose") viewModel.isShowAuthView = false } } else { VStack(spacing: 0) { // Header MyPageHeaderView( isShowButton: !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ) if let notice = viewModel.latestNotice { // Update Banner UpdateBannerView(item: notice) } ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 32) { // Profile Section if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { ProfileSectionView( nickname: viewModel.nickname, profileUrl: viewModel.profileUrl ) { viewModel.getMypage() } .padding(.horizontal, 24) } else { HStack { Text("LOGIN") .font(.custom(Font.preBold.rawValue, size: 32)) .foregroundColor(Color.gray77) } .padding(.vertical, 12) .frame(maxWidth: .infinity) .background(Color.gray22) .cornerRadius(16) .padding(.horizontal, 24) .onTapGesture { AppState.shared .setAppStep(step: .login) } } // Can & Point Cards CanPointCardsView( can: viewModel.chargeCan + viewModel.rewardCan, point: viewModel.point, token: token, refresh: { viewModel.getMypage() } ) .padding(.horizontal, 24) if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { // Category Buttons CategoryButtonsView( isShowAuthView: $viewModel.isShowAuthView, isAuthenticated: viewModel.isAuth, showMessage: { viewModel.errorMessage = $0 viewModel.isShowPopup = true }, refresh: { viewModel.getMypage() } ) .padding(.horizontal, 24) } if let url = URL(string: "https://blog.naver.com/sodalive_official"), UIApplication.shared.canOpenURL(url) { // Voice On Banner Image("img_introduce_voiceon") .padding(.horizontal, 24) .onTapGesture { UIApplication.shared.open(url) } } if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !recentContentViewModel.recentContents.isEmpty { // Recent 10 Section RecentContentSection(recentContents: recentContentViewModel.recentContents) } } .padding(.vertical, 32) } } .background(Color(hex: "131313")) .onAppear { if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { viewModel.getMypage() recentContentViewModel.fetchRecentContents() } viewModel.getLatestNotice() } } } .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { GeometryReader { geo in HStack { Spacer() Text(viewModel.errorMessage) .padding(.vertical, 13.3) .frame(width: geo.size.width - 66.7, alignment: .center) .font(.custom(Font.medium.rawValue, size: 12)) .background(Color.button) .foregroundColor(Color.white) .multilineTextAlignment(.center) .cornerRadius(20) .padding(.top, 66.7) Spacer() } } } .onAppear { payload.applicationId = BOOTPAY_APP_ID payload.price = 0 payload.pg = "다날" payload.method = "본인인증" payload.orderName = "본인인증" payload.authenticationId = "\(UserDefaults.string(forKey: .nickname))__\(String(NSTimeIntervalSince1970))" } } } // MARK: - Header View struct MyPageHeaderView: View { let isShowButton: Bool var body: some View { HStack { // Logo Image("img_text_logo") // 로고 이미지 위치 Spacer() if isShowButton { HStack(spacing: 24) { // Settings Icon Image("ic_settings") .foregroundColor(.white) .frame(width: 24, height: 24) .onTapGesture { AppState.shared.setAppStep(step: .settings) } } } } .padding(.horizontal, 24) .padding(.vertical, 20) } } // MARK: - Update Banner View struct UpdateBannerView: View { let item: NoticeItem var body: some View { HStack { Text("\(item.title)") .font(.system(size: 16)) .foregroundColor(Color(hex: "B0BEC5")) Spacer() HStack(spacing: 2) { Text("자세히") .font(.system(size: 16)) .foregroundColor(Color(hex: "B0BEC5")) Image(systemName: "chevron.right") .foregroundColor(Color(hex: "B0BEC5")) .frame(width: 24, height: 24) } } .padding(.horizontal, 24) .padding(.vertical, 6) .background(Color.black) .contentShape(Rectangle()) .onTapGesture { AppState.shared.setAppStep(step: .noticeDetail(notice: item)) } } } // MARK: - Profile Section View struct ProfileSectionView: View { let nickname: String let profileUrl: String let refresh: () -> Void var body: some View { VStack(spacing: 16) { HStack(spacing: 20) { // Profile Image Placeholder KFImage(URL(string: profileUrl)) .resizable() .frame(width: 64, height: 64) .clipShape(Circle()) VStack(alignment: .leading) { Text("\(nickname)") .font(.system(size: 18, weight: .bold)) .foregroundColor(.white) } Spacer() Button("프로필 수정") { if AppState.shared.roomId <= 0 { AppState.shared.setAppStep(step: .profileUpdate(refresh: refresh)) } } .padding(.horizontal, 12) .padding(.vertical, 6) .background(Color(hex: "263238")) .foregroundColor(.white) .cornerRadius(9999) .font(.system(size: 16)) } } } } // MARK: - Can Point Cards View struct CanPointCardsView: View { let can: Int let point: Int let token: String let refresh: () -> Void var body: some View { // Can & Point Cards VStack(spacing: 0) { // Can Card HStack { HStack(spacing: 8) { Image("ic_can") Text("\(can)") .font(.system(size: 18, weight: .bold)) .foregroundColor(.white) Image(systemName: "chevron.right") .foregroundColor(Color(hex: "B0BEC5")) .frame(width: 24, height: 24) } .onTapGesture { if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { AppState.shared.setAppStep(step: .canStatus(refresh: refresh)) } else { AppState.shared .setAppStep(step: .login) } } Spacer() if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { Button("캔 충전") { AppState.shared.setAppStep(step: .canCharge(refresh: refresh)) } .padding(.horizontal, 16) .padding(.vertical, 11) .background(Color.button) .cornerRadius(9999) .foregroundColor(.white) .font(.system(size: 16, weight: .bold)) } else { Text("") .padding(.horizontal, 16) .padding(.vertical, 11) .background(Color.clear) .foregroundColor(.white) .font(.system(size: 16, weight: .bold)) } } .padding(15) // Point Card HStack { HStack(spacing: 8) { Image("ic_point") Text("\(point)") .font(.system(size: 18, weight: .bold)) .foregroundColor(.white) Image(systemName: "chevron.right") .foregroundColor(Color(hex: "B0BEC5")) .frame(width: 24, height: 24) } .onTapGesture { if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { AppState.shared.setAppStep(step: .pointStatus(refresh: refresh)) } else { AppState.shared .setAppStep(step: .login) } } Spacer() Text("") .padding(.horizontal, 16) .padding(.vertical, 11) .background(Color.clear) .foregroundColor(.white) .font(.system(size: 16, weight: .bold)) } .padding(15) } .background(Color(hex: "263238")) .cornerRadius(16) } } // MARK: - Category Buttons View struct CategoryButtonsView: View { @Binding var isShowAuthView: Bool let isAuthenticated: Bool let showMessage: (String) -> Void let refresh: () -> Void var body: some View { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 16) { CategoryButtonItem(icon: "ic_my_storage", title: "보관함") { AppState.shared.setAppStep(step: .myBox(currentTab: .orderlist)) } CategoryButtonItem(icon: "ic_my_block", title: "차단목록") { AppState.shared.setAppStep(step: .blockList) } CategoryButtonItem( icon: "ic_my_coupon", title: "쿠폰등록" ) { if isAuthenticated { AppState.shared.setAppStep(step: .canCoupon(refresh: refresh)) } else { showMessage("본인인증 후 등록합니다.") DispatchQueue.main.asyncAfter(deadline: .now() + 1) { isShowAuthView = true } } } CategoryButtonItem(icon: "ic_my_notice", title: "공지사항") { AppState.shared.setAppStep(step: .notices) } CategoryButtonItem(icon: "ic_my_event", title: "이벤트") { AppState.shared.setAppStep(step: .events) } CategoryButtonItem(icon: "ic_my_service_center", title: "고객센터") { AppState.shared.setAppStep(step: .serviceCenter) } CategoryButtonItem( icon: "ic_my_auth", title: isAuthenticated ? "인증완료" : "본인인증" ) { if !isAuthenticated { isShowAuthView = true } } } } } struct CategoryButtonItem: View { let icon: String let title: String let onClick: () -> Void var body: some View { VStack(spacing: 12) { // Icon Placeholder RoundedRectangle(cornerRadius: 16) .foregroundColor(Color(hex: "15202F")) .frame(width: 76, height: 76) .overlay{ Image(icon) } Text(title) .font(.custom(Font.preRegular.rawValue, size: 14)) .foregroundColor(.white) } .onTapGesture { onClick() } } } // MARK: - Recent 10 Content Section struct RecentContentSection: View { let recentContents: [RecentContent] var body: some View { VStack(alignment: .leading, spacing: 14) { HStack(spacing: 0) { Text("최근 들은 ") .font(.custom(Font.preBold.rawValue, size: 16)) .foregroundColor(Color(hex: "B0BEC5")) Text("\(recentContents.count)") .font(.custom(Font.preBold.rawValue, size: 16)) .foregroundColor(Color(hex: "FDC118")) } .padding(.horizontal, 24) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { ForEach(0..