diff --git a/SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/Contents.json new file mode 100644 index 0000000..b394621 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_service_center_kakao.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/ic_service_center_kakao.png b/SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/ic_service_center_kakao.png new file mode 100644 index 0000000..cf85945 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/ic_service_center_kakao.png differ diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index 3325905..a3547db 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -51,4 +51,6 @@ enum AppStep { case liveReservation case liveReservationCancel(reservationId: Int) + + case serviceCenter } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 4e07bf5..ba31642 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -80,6 +80,9 @@ struct ContentView: View { case .liveReservationCancel(let reservationId): LiveReservationCancelView(reservationId: reservationId) + case .serviceCenter: + ServiceCenterView() + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/MyPage/ServiceCenter/Faq.swift b/SodaLive/Sources/MyPage/ServiceCenter/Faq.swift new file mode 100644 index 0000000..82d9be6 --- /dev/null +++ b/SodaLive/Sources/MyPage/ServiceCenter/Faq.swift @@ -0,0 +1,13 @@ +// +// Faq.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation + +struct Faq: Decodable, Hashable { + let question: String + let answer: String +} diff --git a/SodaLive/Sources/MyPage/ServiceCenter/FaqApi.swift b/SodaLive/Sources/MyPage/ServiceCenter/FaqApi.swift new file mode 100644 index 0000000..4b280e2 --- /dev/null +++ b/SodaLive/Sources/MyPage/ServiceCenter/FaqApi.swift @@ -0,0 +1,48 @@ +// +// FaqApi.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation +import Moya + +enum FaqApi { + case faqs(category: String) + case faqCategories +} + +extension FaqApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .faqs: + return "/faq" + + case .faqCategories: + return "/faq/category" + } + } + + var method: Moya.Method { + return .get + } + + var task: Task { + switch self { + case .faqCategories: + return .requestPlain + + case .faqs(let category): + return .requestParameters(parameters: ["category" : category] as [String : Any], encoding: URLEncoding.queryString) + } + } + + var headers: [String : String]? { + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } +} diff --git a/SodaLive/Sources/MyPage/ServiceCenter/FaqRepository.swift b/SodaLive/Sources/MyPage/ServiceCenter/FaqRepository.swift new file mode 100644 index 0000000..097a3c2 --- /dev/null +++ b/SodaLive/Sources/MyPage/ServiceCenter/FaqRepository.swift @@ -0,0 +1,23 @@ +// +// FaqRepository.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +final class FaqRepository { + private let api = MoyaProvider<FaqApi>() + + func getFaqs(category: String) -> AnyPublisher<Response, MoyaError> { + return api.requestPublisher(.faqs(category: category)) + } + + func getFaqCategories() -> AnyPublisher<Response, MoyaError> { + return api.requestPublisher(.faqCategories) + } +} diff --git a/SodaLive/Sources/MyPage/ServiceCenter/FaqView.swift b/SodaLive/Sources/MyPage/ServiceCenter/FaqView.swift new file mode 100644 index 0000000..7d9d703 --- /dev/null +++ b/SodaLive/Sources/MyPage/ServiceCenter/FaqView.swift @@ -0,0 +1,85 @@ +// +// FaqView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI +import RichText + +struct FaqView: View { + + let faqs: [Faq] + + @State private var openIndex = -1 + + var body: some View { + VStack(spacing: 0) { + ForEach(0..<faqs.count, id: \.self) { index in + let faq = faqs[index] + + VStack(spacing: 0) { + HStack(alignment: .top, spacing: 6.7) { + Text("Q") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "9970ff")) + + Text(faq.question) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + .fixedSize(horizontal: false, vertical: true) + + Spacer() + + Image(openIndex == index ? "btn_dropdown_up" : "btn_dropdown_down") + .resizable() + .frame(width: 20, height: 20) + } + .padding(.vertical, 20) + .padding(.horizontal, 6.7) + + if openIndex == index { + HStack(alignment: .top, spacing: 6.7) { + Text("A") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.top, 13.3) + + RichText(html: faq.answer) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "bbbbbb")) + } + .padding(.vertical, 20) + .padding(.horizontal, 6.7) + .background(Color(hex: "222222")) + } + } + .padding(.horizontal, 13.3) + .contentShape(Rectangle()) + .onTapGesture { + if openIndex != index { + openIndex = index + } else { + openIndex = -1 + } + } + } + } + .padding(.bottom, 40) + } +} + +struct FaqView_Previews: PreviewProvider { + static var previews: some View { + FaqView( + faqs: [ + Faq(question: "질문1", answer: "답변1"), + Faq(question: "질문2", answer: "답변2"), + Faq(question: "질문3", answer: "답변3"), + Faq(question: "질문4", answer: "답변4"), + Faq(question: "질문5", answer: "답변5") + ] + ) + } +} diff --git a/SodaLive/Sources/MyPage/ServiceCenterButtonView.swift b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterButtonView.swift similarity index 94% rename from SodaLive/Sources/MyPage/ServiceCenterButtonView.swift rename to SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterButtonView.swift index 77eb9b0..ec3befc 100644 --- a/SodaLive/Sources/MyPage/ServiceCenterButtonView.swift +++ b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterButtonView.swift @@ -29,6 +29,7 @@ struct ServiceCenterButtonView: View { .background(Color(hex: "664aab")) .cornerRadius(6.7) .onTapGesture { + AppState.shared.setAppStep(step: .serviceCenter) } } } diff --git a/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterCategoryItemView.swift b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterCategoryItemView.swift new file mode 100644 index 0000000..c67c396 --- /dev/null +++ b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterCategoryItemView.swift @@ -0,0 +1,31 @@ +// +// ServiceCenterCategoryItemView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct ServiceCenterCategoryItemView: View { + + let category: String + let isSelected: Bool + + var body: some View { + GeometryReader { proxy in + Text(category) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.white) + .frame(width: proxy.size.width, height: 46.7) + .background(isSelected ? Color(hex: "9970ff") : Color(hex: "222222")) + .cornerRadius(4.7) + } + } +} + +struct ServiceCenterCategoryItemView_Previews: PreviewProvider { + static var previews: some View { + ServiceCenterCategoryItemView(category: "전체", isSelected: false) + } +} diff --git a/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterCategoryView.swift b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterCategoryView.swift new file mode 100644 index 0000000..00661a4 --- /dev/null +++ b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterCategoryView.swift @@ -0,0 +1,48 @@ +// +// ServiceCenterCategoryView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct ServiceCenterCategoryView: View { + let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + ] + + let categories: [String] + + @Binding var selectedCategory: String + + var body: some View { + LazyVGrid(columns: columns, spacing: 10) { + ForEach(categories, id: \.self) { category in + ServiceCenterCategoryItemView( + category: category, + isSelected: selectedCategory == category + ) + .frame(height: 46.7) + .onTapGesture { + if selectedCategory != category { + selectedCategory = category + } + } + } + } + .padding(.horizontal, 13.3) + } +} + +struct ServiceCenterCategoryView_Previews: PreviewProvider { + static var previews: some View { + ServiceCenterCategoryView( + categories: ["전체", "사용방법", "수다", "결제/환불", "서비스/기타"], + selectedCategory: .constant("전체") + ) + } +} diff --git a/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterView.swift b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterView.swift new file mode 100644 index 0000000..bf089ac --- /dev/null +++ b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterView.swift @@ -0,0 +1,99 @@ +// +// ServiceCenterView.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import SwiftUI + +struct ServiceCenterView: View { + + @ObservedObject var viewModel = ServiceCenterViewModel() + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + VStack(spacing: 0) { + DetailNavigationBar(title: "고객센터") + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + Image("ic_logo") + .resizable() + .scaledToFill() + .frame(width: 106.7, height: 106.7, alignment: .top) + + Text("고객센터") + .font(.custom(Font.bold.rawValue, size: 20)) + .foregroundColor(Color(hex: "eeeeee")) + + HStack(spacing: 13.3) { + Image("ic_service_center_kakao") + .resizable() + .scaledToFill() + .frame(width: 21, height: 18.8, alignment: .top) + + Text("TALK 문의") + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(.black) + } + .padding(.vertical, 14) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "ffe368")) + .cornerRadius(8) + .padding(.top, 20) + .onTapGesture { + UIApplication.shared.open(URL(string: "http://pf.kakao.com/_sZaeb")!) + } + + Rectangle() + .frame(width: screenSize().width, height: 6.7) + .foregroundColor(Color(hex: "232323")) + .padding(.vertical, 20) + + Text("자주 묻는 질문") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 26.7, alignment: .leading) + + ServiceCenterCategoryView( + categories: viewModel.categories, + selectedCategory: $viewModel.selectedCategory + ) + .padding(.vertical, 20) + + FaqView(faqs: viewModel.faqs) + } + } + } + } + .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() + } + } + } + .onAppear { + viewModel.getFaqCategories() + } + } +} + +struct ServiceCenterView_Previews: PreviewProvider { + static var previews: some View { + ServiceCenterView() + } +} diff --git a/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterViewModel.swift b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterViewModel.swift new file mode 100644 index 0000000..b7cc95a --- /dev/null +++ b/SodaLive/Sources/MyPage/ServiceCenter/ServiceCenterViewModel.swift @@ -0,0 +1,108 @@ +// +// ServiceCenterViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/11. +// + +import Foundation +import Combine + +final class ServiceCenterViewModel: ObservableObject { + + private let repository = FaqRepository() + private var subscription = Set<AnyCancellable>() + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isLoading = false + + @Published var faqs = [Faq]() + @Published var categories = [String]() + @Published var selectedCategory = "" { + didSet { + getFaqs() + } + } + + func getFaqCategories() { + categories.removeAll() + isLoading = true + + repository.getFaqCategories() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse<[String]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.categories.append(contentsOf: data) + if !data.isEmpty { + self.selectedCategory = data[0] + } + } 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 getFaqs() { + faqs.removeAll() + isLoading = true + + repository.getFaqs(category: self.selectedCategory) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse<[Faq]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.faqs.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) + } +}