feat: 포인트 내역 UI 추가
This commit is contained in:
@@ -157,4 +157,6 @@ enum AppStep {
|
||||
case introduceCreatorAll
|
||||
|
||||
case message
|
||||
|
||||
case pointStatus(refresh: () -> Void)
|
||||
}
|
||||
|
||||
@@ -242,6 +242,9 @@ struct ContentView: View {
|
||||
case .message:
|
||||
MessageView()
|
||||
|
||||
case .pointStatus(let refresh):
|
||||
PointStatusView(refresh: refresh)
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||
|
||||
@@ -28,7 +28,7 @@ struct CanStatusView: View {
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
|
||||
Text("\(viewModel.totalCan) 캔")
|
||||
Text("\(viewModel.totalCan)")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
}
|
||||
|
||||
@@ -124,14 +124,23 @@ struct MyPageView: View {
|
||||
.frame(width: screenSize().width - 26.7)
|
||||
.padding(.top, 26.7)
|
||||
|
||||
HStack(spacing: 6.7) {
|
||||
Text("\(data.point)")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Image("ic_point")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
HStack {
|
||||
HStack(spacing: 6.7) {
|
||||
Text("\(data.point)")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Image("ic_point")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Image("ic_forward")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .pointStatus(refresh: { viewModel.getMypage() }))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
10
SodaLive/Sources/MyPage/Point/GetPointStatusResponse.swift
Normal file
10
SodaLive/Sources/MyPage/Point/GetPointStatusResponse.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// GetPointStatusResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/25.
|
||||
//
|
||||
|
||||
struct GetPointStatusResponse: Decodable {
|
||||
let point: Int
|
||||
}
|
||||
55
SodaLive/Sources/MyPage/Point/PointStatusApi.swift
Normal file
55
SodaLive/Sources/MyPage/Point/PointStatusApi.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// PointStatusApi.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Moya
|
||||
|
||||
enum PointStatusApi {
|
||||
case getPointStatus
|
||||
case getPointRewardStatus
|
||||
case getPointUseStatus
|
||||
}
|
||||
|
||||
extension PointStatusApi: TargetType {
|
||||
var baseURL: URL {
|
||||
return URL(string: BASE_URL)!
|
||||
}
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .getPointStatus:
|
||||
return "/point/status"
|
||||
|
||||
case .getPointRewardStatus:
|
||||
return "/point/status/reward"
|
||||
|
||||
case .getPointUseStatus:
|
||||
return "/point/status/use"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
return .get
|
||||
}
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case .getPointStatus:
|
||||
return .requestPlain
|
||||
|
||||
case .getPointRewardStatus, .getPointUseStatus:
|
||||
return .requestParameters(
|
||||
parameters: ["timezone": TimeZone.current.identifier],
|
||||
encoding: URLEncoding.queryString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var headers: [String : String]? {
|
||||
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
|
||||
}
|
||||
}
|
||||
27
SodaLive/Sources/MyPage/Point/PointStatusRepository.swift
Normal file
27
SodaLive/Sources/MyPage/Point/PointStatusRepository.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// PointStatusRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
class PointStatusRepository {
|
||||
private let api = MoyaProvider<PointStatusApi>()
|
||||
|
||||
func getPointStatus() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getPointStatus)
|
||||
}
|
||||
|
||||
func getPointRewardStatus() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getPointRewardStatus)
|
||||
}
|
||||
|
||||
func getPointUseStatus() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getPointUseStatus)
|
||||
}
|
||||
}
|
||||
135
SodaLive/Sources/MyPage/Point/PointStatusView.swift
Normal file
135
SodaLive/Sources/MyPage/Point/PointStatusView.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// PointStatusView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PointStatusView: View {
|
||||
|
||||
let refresh: () -> Void
|
||||
|
||||
@StateObject var viewModel = PointStatusViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
GeometryReader { proxy in
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: "포인트 내역") {
|
||||
AppState.shared.setAppStep(step: .main)
|
||||
}
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 26.7) {
|
||||
HStack(spacing: 6.7) {
|
||||
Image("ic_point")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
|
||||
Text("\(viewModel.totalCan)")
|
||||
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||
.foregroundColor(.grayee)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 13.3)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 13.3)
|
||||
.background(Color.gray22)
|
||||
.cornerRadius(16.7)
|
||||
.padding(.top, 13.3)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
Text("받은내역")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(viewModel.currentTab == .reward ? .grayee : .gray77)
|
||||
Spacer()
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(
|
||||
.button
|
||||
.opacity(viewModel.currentTab == .reward ? 1 : 0)
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.onTapGesture {
|
||||
if viewModel.currentTab != .reward {
|
||||
viewModel.currentTab = .reward
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
Text("사용내역")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(viewModel.currentTab == .use ? .grayee : .gray77)
|
||||
Spacer()
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(
|
||||
.button
|
||||
.opacity(viewModel.currentTab == .use ? 1 : 0)
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.onTapGesture {
|
||||
if viewModel.currentTab != .use {
|
||||
viewModel.currentTab = .use
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
|
||||
switch viewModel.currentTab {
|
||||
case .reward:
|
||||
PointRewardStatusView()
|
||||
|
||||
case .use:
|
||||
PointUseStatusView()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if proxy.safeAreaInsets.bottom > 0 {
|
||||
Rectangle()
|
||||
.foregroundColor(.black)
|
||||
.frame(width: proxy.size.width, height: 15.3)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
||||
GeometryReader { geo in
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(viewModel.errorMessage)
|
||||
.padding(.vertical, 13.3)
|
||||
.padding(.horizontal, 6.7)
|
||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(20)
|
||||
.padding(.top, 66.7)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.getPointStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PointStatusView {}
|
||||
}
|
||||
143
SodaLive/Sources/MyPage/Point/PointStatusViewModel.swift
Normal file
143
SodaLive/Sources/MyPage/Point/PointStatusViewModel.swift
Normal file
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// PointStatusViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class PointStatusViewModel: ObservableObject {
|
||||
private let repository = PointStatusRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var currentTab: CurrentTab = .reward
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var totalCan: Int = 0
|
||||
|
||||
@Published var useStatusItems: [GetPointUseStatusResponse] = []
|
||||
@Published var rewardStatusItems: [GetPointRewardStatusResponse] = []
|
||||
|
||||
func getPointStatus() {
|
||||
isLoading = true
|
||||
|
||||
repository.getPointStatus()
|
||||
.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<GetPointStatusResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.totalCan = data.point
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getPointRewardStatus() {
|
||||
isLoading = true
|
||||
|
||||
repository.getPointRewardStatus()
|
||||
.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<[GetPointRewardStatusResponse]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.rewardStatusItems.append(contentsOf: data)
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func getPointUseStatus() {
|
||||
isLoading = true
|
||||
|
||||
repository.getPointUseStatus()
|
||||
.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<[GetPointUseStatusResponse]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.useStatusItems.append(contentsOf: data)
|
||||
} else {
|
||||
if let message = decoded.message {
|
||||
self.errorMessage = message
|
||||
} else {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
}
|
||||
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
enum CurrentTab: String {
|
||||
case reward, use
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// GetPointRewardStatusResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/25.
|
||||
//
|
||||
|
||||
struct GetPointRewardStatusResponse: Decodable, Hashable {
|
||||
let rewardPoint: String
|
||||
let date: String
|
||||
let method: String
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// PointRewardStatusView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PointRewardStatusView: View {
|
||||
|
||||
@StateObject var viewModel = PointStatusViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack(spacing: 13.3) {
|
||||
ForEach(viewModel.rewardStatusItems, id: \.self) { item in
|
||||
PointRewardStatusItemView(item: item)
|
||||
}
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
|
||||
if viewModel.isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.getPointRewardStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PointRewardStatusItemView: View {
|
||||
|
||||
let item: GetPointRewardStatusResponse
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 6.7) {
|
||||
Text(item.rewardPoint)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Text(item.date)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(item.method)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.grayee)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color.gray11)
|
||||
.cornerRadius(16.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PointRewardStatusView()
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// GetPointUseStatusResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/25.
|
||||
//
|
||||
|
||||
struct GetPointUseStatusResponse: Decodable, Hashable {
|
||||
let title: String
|
||||
let date: String
|
||||
let point: Int
|
||||
}
|
||||
71
SodaLive/Sources/MyPage/Point/Use/PointUseStatusView.swift
Normal file
71
SodaLive/Sources/MyPage/Point/Use/PointUseStatusView.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// PointUseStatusView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 5/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PointUseStatusView: View {
|
||||
|
||||
@StateObject var viewModel = PointStatusViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack(spacing: 13.3) {
|
||||
ForEach(viewModel.useStatusItems, id: \.self) { item in
|
||||
PointUseStatusItemView(item: item)
|
||||
}
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
|
||||
if viewModel.isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.getPointUseStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PointUseStatusItemView: View {
|
||||
|
||||
let item: GetPointUseStatusResponse
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 6.7) {
|
||||
Text(item.title)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Text(item.date)
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(item.point)")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Image("ic_point")
|
||||
.resizable()
|
||||
.frame(width: 26.7, height: 26.7)
|
||||
.padding(.leading, 6.7)
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color.gray11)
|
||||
.cornerRadius(16.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PointUseStatusView()
|
||||
}
|
||||
Reference in New Issue
Block a user