diff --git a/SodaLive/Resources/Assets.xcassets/ic_coupon.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_coupon.imageset/Contents.json new file mode 100644 index 0000000..d7744fd --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_coupon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_coupon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_coupon.imageset/ic_coupon.png b/SodaLive/Resources/Assets.xcassets/ic_coupon.imageset/ic_coupon.png new file mode 100644 index 0000000..0ced128 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_coupon.imageset/ic_coupon.png differ diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index 78e0f70..f685933 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -117,4 +117,6 @@ enum AppStep { case creatorCommunityWrite(onSuccess: () -> Void) case creatorCommunityModify(postId: Int, onSuccess: () -> Void) + + case canCoupon } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 04910ed..9e2d228 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -172,6 +172,9 @@ struct ContentView: View { case .creatorCommunityModify(let postId, let onSuccess): CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess) + case .canCoupon: + CanCouponView() + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/MyPage/Can/CanApi.swift b/SodaLive/Sources/MyPage/Can/CanApi.swift index 68cf356..27072cb 100644 --- a/SodaLive/Sources/MyPage/Can/CanApi.swift +++ b/SodaLive/Sources/MyPage/Can/CanApi.swift @@ -19,6 +19,8 @@ enum CanApi { case pgChargeCan(request: PgChargeRequest) case pgVerify(request: PgVerifyRequest) + + case useCanCoupon(request: UseCanCouponRequest) } extension CanApi: TargetType { @@ -51,6 +53,9 @@ extension CanApi: TargetType { case .pgVerify: return "/charge/verify" + + case .useCanCoupon: + return "/can/coupon/use" } } @@ -59,7 +64,7 @@ extension CanApi: TargetType { case .getCanStatus, .getCanChargeStatus, .getCanUseStatus, .getCans: return .get - case .chargeCan, .verify, .pgChargeCan, .pgVerify: + case .chargeCan, .verify, .pgChargeCan, .pgVerify, .useCanCoupon: return .post } } @@ -97,6 +102,9 @@ extension CanApi: TargetType { case .pgVerify(let request): return .requestJSONEncodable(request) + + case .useCanCoupon(let request): + return .requestJSONEncodable(request) } } diff --git a/SodaLive/Sources/MyPage/Can/CanRepository.swift b/SodaLive/Sources/MyPage/Can/CanRepository.swift index b5e4f08..2c34207 100644 --- a/SodaLive/Sources/MyPage/Can/CanRepository.swift +++ b/SodaLive/Sources/MyPage/Can/CanRepository.swift @@ -44,5 +44,10 @@ final class CanRepository { func getCans() -> AnyPublisher { return api.requestPublisher(.getCans) } + + func useCanCoupon(couponNumber: String) -> AnyPublisher { + let request = UseCanCouponRequest(couponNumber: couponNumber) + return api.requestPublisher(.useCanCoupon(request: request)) + } } diff --git a/SodaLive/Sources/MyPage/Can/Coupon/CanChargeCouponButtonView.swift b/SodaLive/Sources/MyPage/Can/Coupon/CanChargeCouponButtonView.swift new file mode 100644 index 0000000..e2b5d2b --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Coupon/CanChargeCouponButtonView.swift @@ -0,0 +1,34 @@ +// +// CanChargeCouponButtonView.swift +// SodaLive +// +// Created by klaus on 2024/01/03. +// + +import SwiftUI + +struct CanChargeCouponButtonView: View { + var body: some View { + HStack(spacing: 5.3) { + Text("쿠폰 등록") + .font(.custom(Font.bold.rawValue, size: 16)) + .foregroundColor(Color(hex: "eeeeee")) + + Image("ic_coupon") + + Spacer() + + Image("ic_forward") + } + .padding(.horizontal, 13.3) + .padding(.vertical, 20) + .background(Color(hex: "13181b")) + .cornerRadius(6.7) + } +} + +struct CanChargeCouponButtonView_Previews: PreviewProvider { + static var previews: some View { + CanChargeCouponButtonView() + } +} diff --git a/SodaLive/Sources/MyPage/Can/Coupon/CanCouponNoticeItemView.swift b/SodaLive/Sources/MyPage/Can/Coupon/CanCouponNoticeItemView.swift new file mode 100644 index 0000000..f0b99af --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Coupon/CanCouponNoticeItemView.swift @@ -0,0 +1,34 @@ +// +// CanCouponNoticeItemView.swift +// SodaLive +// +// Created by klaus on 2024/01/03. +// + +import SwiftUI + +struct CanCouponNoticeItemView: View { + + let notice: String + + var body: some View { + HStack(alignment: .top, spacing: 4) { + Text("-") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Text(notice) + .multilineTextAlignment(.leading) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.horizontal, 10) + } +} + +struct CanCouponNoticeItemView_Previews: PreviewProvider { + static var previews: some View { + CanCouponNoticeItemView(notice: "본인인증한 계정 1회에 한해 이벤트 쿠폰등록이 적용 됩니다.") + } +} diff --git a/SodaLive/Sources/MyPage/Can/Coupon/CanCouponView.swift b/SodaLive/Sources/MyPage/Can/Coupon/CanCouponView.swift new file mode 100644 index 0000000..a1b527b --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Coupon/CanCouponView.swift @@ -0,0 +1,132 @@ +// +// CanCouponView.swift +// SodaLive +// +// Created by klaus on 2024/01/03. +// + +import SwiftUI + +struct CanCouponView: View { + + @StateObject var viewModel = CanCouponViewModel() + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + GeometryReader { proxy in + VStack(spacing: 0) { + DetailNavigationBar(title: "쿠폰등록") + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + Text("쿠폰번호 입력") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(maxWidth: .infinity, alignment: .leading) + + TextField("쿠폰번호를 입력하세요", text: $viewModel.couponNumber) + .autocapitalization(.allCharacters) // Force uppercase keyboard + .textContentType(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.vertical, 16.7) + .padding(.horizontal, 13.3) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + .keyboardType(.default) + .padding(.top, 13.3) + .onChange(of: viewModel.couponNumber) { newString in + let filtered = newString.filter { $0.isUppercase || $0.isNumber } + if filtered != newString { + viewModel.couponNumber = filtered + } + } + + Text("등록하기") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(.white) + .padding(.vertical, 16) + .frame(maxWidth: .infinity) + .background(Color(hex: "3bb9f1")) + .cornerRadius(10) + .padding(.top, 21.3) + .onTapGesture { + viewModel.useCoupon() + } + + VStack(alignment: .leading, spacing: 0) { + Text("등록안내") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + + CanCouponNoticeItemView(notice: "공백없이 쿠폰번호 16자리를 입력해주세요.") + .padding(.top, 8) + + CanCouponNoticeItemView(notice: "입력한 쿠폰은 소멸시까지 취소처리가 되지 않습니다.") + .padding(.top, 4) + + CanCouponNoticeItemView(notice: "충전된 캔은 캔 충전현황에서 확인할 수 있습니다.") + .padding(.top, 4) + + Rectangle() + .foregroundColor(.clear) + .frame(maxWidth: .infinity, minHeight: 1, maxHeight: 1) + .background(Color(hex: "555555")) + .padding(.vertical, 26.7) + + Text("주의사항") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + + CanCouponNoticeItemView(notice: "이벤트 쿠폰을 통해 충전한 캔은 환불되지 않습니다.") + .padding(.top, 8) + + CanCouponNoticeItemView(notice: "쿠폰은 상업적 용도로 사용하거나 매매할 수 없습니다.") + .padding(.top, 4) + + CanCouponNoticeItemView(notice: "한번 등록한 쿠폰은 재사용이 불가합니다.") + .padding(.top, 4) + + CanCouponNoticeItemView(notice: "연령 제한 정책에 따라 쿠폰이용은 본인인증한 회원만 이용 가능합니다.") + .padding(.top, 4) + } + .padding(.horizontal, 13.3) + .padding(.vertical, 26.7) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(hex: "13181b")) + .cornerRadius(6.7) + .padding(.top, 21.3) + } + .padding(13.3) + } + } + .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(hex: "9970ff")) + .foregroundColor(Color.white) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + } + } + } +} + +struct CanCouponView_Previews: PreviewProvider { + static var previews: some View { + CanCouponView() + } +} diff --git a/SodaLive/Sources/MyPage/Can/Coupon/CanCouponViewModel.swift b/SodaLive/Sources/MyPage/Can/Coupon/CanCouponViewModel.swift new file mode 100644 index 0000000..b3ca551 --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Coupon/CanCouponViewModel.swift @@ -0,0 +1,65 @@ +// +// CanCouponViewModel.swift +// SodaLive +// +// Created by klaus on 2024/01/03. +// + +import Foundation +import Combine + +final class CanCouponViewModel: ObservableObject { + + private let repository = CanRepository() + private var subscription = Set() + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isLoading = false + + @Published var couponNumber = "" + + func useCoupon() { + if !isLoading { + isLoading = true + repository.useCanCoupon(couponNumber: couponNumber) + .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(ApiResponseWithoutData.self, from: responseData) + + if decoded.success { + self.errorMessage = "해당 쿠폰의 캔이 충전되었습니다." + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + AppState.shared.setAppStep(step: .canStatus(refresh: {})) + } + } 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) + } + } + +} diff --git a/SodaLive/Sources/MyPage/Can/Coupon/UseCanCouponRequest.swift b/SodaLive/Sources/MyPage/Can/Coupon/UseCanCouponRequest.swift new file mode 100644 index 0000000..c456ea3 --- /dev/null +++ b/SodaLive/Sources/MyPage/Can/Coupon/UseCanCouponRequest.swift @@ -0,0 +1,10 @@ +// +// UseCanCouponRequest.swift +// SodaLive +// +// Created by klaus on 2024/01/03. +// + +struct UseCanCouponRequest: Encodable { + let couponNumber: String +} diff --git a/SodaLive/Sources/MyPage/CanCardView.swift b/SodaLive/Sources/MyPage/CanCardView.swift index 931da83..763ce85 100644 --- a/SodaLive/Sources/MyPage/CanCardView.swift +++ b/SodaLive/Sources/MyPage/CanCardView.swift @@ -52,9 +52,9 @@ struct CanCardView: View { } } .padding(.horizontal, 13.3) - .padding(.vertical, 16.7) + .padding(.vertical, 10) .background(Color(hex: "222222")) - .cornerRadius(16.7) + .cornerRadius(6.7) } } diff --git a/SodaLive/Sources/MyPage/MyPageView.swift b/SodaLive/Sources/MyPage/MyPageView.swift index 1302385..2838c91 100644 --- a/SodaLive/Sources/MyPage/MyPageView.swift +++ b/SodaLive/Sources/MyPage/MyPageView.swift @@ -87,6 +87,15 @@ struct MyPageView: View { .frame(width: screenSize().width - 26.7) .padding(.top, 26.7) + if data.isAuth { + CanChargeCouponButtonView() + .frame(width: screenSize().width - 26.7) + .padding(.top, 13.3) + .onTapGesture { + AppState.shared.setAppStep(step: .canCoupon) + } + } + ReservationStatusView(data: data) .padding(.top, 33.3) diff --git a/SodaLive/Sources/MyPage/ReservationStatusView.swift b/SodaLive/Sources/MyPage/ReservationStatusView.swift index 3e3ef16..d7d55e8 100644 --- a/SodaLive/Sources/MyPage/ReservationStatusView.swift +++ b/SodaLive/Sources/MyPage/ReservationStatusView.swift @@ -25,8 +25,8 @@ struct ReservationStatusView: View { .frame(width: 20, height: 20) Text("라이브") - .font(.custom(Font.medium.rawValue, size: 14.7)) - .foregroundColor(Color(hex: "eeeeee")) + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "3bb9f1")) Text("\(data.liveReservationCount)") .font(.custom(Font.medium.rawValue, size: 14.7))