고객센터 페이지 추가
This commit is contained in:
parent
e082e07fa6
commit
64b0380671
21
SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/ic_service_center_kakao.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_service_center_kakao.imageset/ic_service_center_kakao.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 514 B |
|
@ -51,4 +51,6 @@ enum AppStep {
|
||||||
case liveReservation
|
case liveReservation
|
||||||
|
|
||||||
case liveReservationCancel(reservationId: Int)
|
case liveReservationCancel(reservationId: Int)
|
||||||
|
|
||||||
|
case serviceCenter
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,9 @@ struct ContentView: View {
|
||||||
case .liveReservationCancel(let reservationId):
|
case .liveReservationCancel(let reservationId):
|
||||||
LiveReservationCancelView(reservationId: reservationId)
|
LiveReservationCancelView(reservationId: reservationId)
|
||||||
|
|
||||||
|
case .serviceCenter:
|
||||||
|
ServiceCenterView()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
.frame(width: 0, height: 0, alignment: .topLeading)
|
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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))"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ struct ServiceCenterButtonView: View {
|
||||||
.background(Color(hex: "664aab"))
|
.background(Color(hex: "664aab"))
|
||||||
.cornerRadius(6.7)
|
.cornerRadius(6.7)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(step: .serviceCenter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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("전체")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue