커뮤니티 수정 UI, API 추가

This commit is contained in:
Yu Sung 2023-12-21 21:04:08 +09:00
parent f40642f90f
commit 7bd32f8486
12 changed files with 474 additions and 3 deletions

View File

@ -115,4 +115,6 @@ enum AppStep {
case creatorCommunityAll(creatorId: Int)
case creatorCommunityWrite(onSuccess: () -> Void)
case creatorCommunityModify(postId: Int, onSuccess: () -> Void)
}

View File

@ -169,6 +169,9 @@ struct ContentView: View {
case .creatorCommunityWrite(let onSuccess):
CreatorCommunityWriteView(onSuccess: onSuccess)
case .creatorCommunityModify(let postId, let onSuccess):
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
default:
EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading)

View File

@ -125,6 +125,8 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider {
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!",
date: "3일전",
isCommentAvailable: false,
isAdult: false,
isLike: true,
likeCount: 10,
commentCount: 0,

View File

@ -71,6 +71,15 @@ struct CreatorCommunityAllView: View {
isShowing: $viewModel.isShowReportMenu,
isShowCreatorMenu: creatorId == UserDefaults.int(forKey: .userId),
modifyAction: {
let postId = viewModel.postId
AppState.shared
.setAppStep(
step: .creatorCommunityModify(
postId: postId,
onSuccess: creatorCommunityModifySuccess
)
)
viewModel.postId = 0
},
deleteAction: {
if creatorId == UserDefaults.int(forKey: .userId) {
@ -137,6 +146,10 @@ struct CreatorCommunityAllView: View {
}
}
}
private func creatorCommunityModifySuccess() {
viewModel.getCommunityPostList()
}
}
struct CreatorCommunityAllView_Previews: PreviewProvider {

View File

@ -12,6 +12,7 @@ enum CreatorCommunityApi {
case createCommunityPost(parameters: [MultipartFormData])
case modifyCommunityPost(parameters: [MultipartFormData])
case getCommunityPostList(creatorId: Int, page: Int, size: Int)
case getCommunityPostDetail(postId: Int)
case communityPostLike(postId: Int)
case createCommunityPostComment(comment: String, postId: Int, parentId: Int?)
case getCommunityPostCommentList(postId: Int, page: Int, size: Int)
@ -40,6 +41,9 @@ extension CreatorCommunityApi: TargetType {
case .getCommentReplyList(let commentId, _, _):
return "/creator-community/comment/\(commentId)"
case .getCommunityPostDetail(let postId):
return "/creator-community/\(postId)"
}
}
@ -48,7 +52,7 @@ extension CreatorCommunityApi: TargetType {
case .createCommunityPost, .communityPostLike, .createCommunityPostComment:
return .post
case .getCommunityPostList, .getCommunityPostCommentList, .getCommentReplyList:
case .getCommunityPostList, .getCommunityPostCommentList, .getCommentReplyList, .getCommunityPostDetail:
return .get
case .modifyComment, .modifyCommunityPost:
@ -99,7 +103,10 @@ extension CreatorCommunityApi: TargetType {
case .modifyComment(let request):
return .requestJSONEncodable(request)
case .getCommunityPostDetail:
let parameters = ["timezone": TimeZone.current.identifier] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
}
}

View File

@ -91,6 +91,8 @@ struct CreatorCommunityItemView_Previews: PreviewProvider {
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
content: "안녕하세요",
date: "3일전",
isCommentAvailable: false,
isAdult: false,
isLike: false,
likeCount: 10,
commentCount: 0,

View File

@ -25,6 +25,10 @@ class CreatorCommunityRepository {
return api.requestPublisher(.getCommunityPostList(creatorId: creatorId, page: page, size: size))
}
func getCommunityPostDetail(postId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getCommunityPostDetail(postId: postId))
}
func communityPostLike(postId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.communityPostLike(postId: postId))
}

View File

@ -12,6 +12,8 @@ struct GetCommunityPostListResponse: Decodable {
let imageUrl: String?
let content: String
let date: String
let isCommentAvailable: Bool
let isAdult: Bool
let isLike: Bool
let likeCount: Int
let commentCount: Int

View File

@ -0,0 +1,271 @@
//
// CreatorCommunityModifyView.swift
// SodaLive
//
// Created by klaus on 2023/12/21.
//
import SwiftUI
import Kingfisher
struct CreatorCommunityModifyView: View {
let postId: Int
@StateObject var keyboardHandler = KeyboardHandler()
@StateObject private var viewModel = CreatorCommunityModifyViewModel()
@State private var isShowPhotoPicker = false
let onSuccess: () -> Void
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
GeometryReader { proxy in
ZStack {
VStack(spacing: 0) {
DetailNavigationBar(title: "게시글 수정")
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
VStack(spacing: 0) {
Text("이미지")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.frame(maxWidth: .infinity, alignment: .leading)
ZStack {
if let selectedImage = viewModel.postImage {
Image(uiImage: selectedImage)
.resizable()
.scaledToFill()
.frame(width: 107, height: 107)
.background(Color(hex: "3e3358"))
.cornerRadius(8)
.clipped()
} else if let postImageUrl = viewModel.postImageUrl {
KFImage(URL(string: postImageUrl))
.resizable()
.scaledToFill()
.frame(width: 107, height: 107)
.background(Color(hex: "3e3358"))
.cornerRadius(8)
.clipped()
} else {
Image("ic_logo2")
.resizable()
.scaledToFit()
.padding(13.3)
.frame(width: 107, height: 107)
.background(Color(hex: "13181B"))
.cornerRadius(8)
.clipped()
}
Image("ic_camera")
.padding(10)
.background(Color(hex: "3BB9F1"))
.cornerRadius(30)
.offset(x: 50, y: 36)
}
.frame(alignment: .bottomTrailing)
.contentShape(Rectangle())
.onTapGesture { isShowPhotoPicker = true }
HStack(alignment: .top, spacing: 0) {
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
Text("등록할 이미지가 없으면 이미지 없이 게시글만 등록 하셔도 됩니다.")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 24)
HStack(spacing: 0) {
Text("내용")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Text("\(viewModel.content.count)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "ff5c49")) +
Text(" / 최대 500자")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777"))
}
.padding(.top, 26.7)
TextViewWrapper(
text: $viewModel.content,
placeholder: viewModel.placeholder,
textColorHex: "eeeeee",
backgroundColorHex: "222222"
)
.frame(height: 184)
.cornerRadius(6.7)
.padding(.top, 13.3)
VStack(spacing: 13.3) {
Text("댓글 가능 여부")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
SelectButtonView(
title: "댓글 가능",
isChecked: viewModel.isAvailableComment
) {
if !viewModel.isAvailableComment {
viewModel.isAvailableComment = true
}
}
SelectButtonView(
title: "댓글 불가",
isChecked: !viewModel.isAvailableComment
) {
if viewModel.isAvailableComment {
viewModel.isAvailableComment = false
}
}
}
}
.padding(.top, 26.7)
VStack(spacing: 13.3) {
Text("연령 제한")
.font(.custom(Font.bold.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 13.3) {
SelectButtonView(
title: "전체 연령",
isChecked: !viewModel.isAdult
) {
if viewModel.isAdult {
viewModel.isAdult = false
}
}
SelectButtonView(
title: "19세 이상",
isChecked: viewModel.isAdult
) {
if !viewModel.isAdult {
viewModel.isAdult = true
}
}
}
}
.padding(.top, 26.7)
}
.padding(13.3)
VStack(spacing: 0) {
HStack(spacing: 13.3) {
Text("닫기")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "3BB9F1"))
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color(hex: "13181B"))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(hex: "3BB9F1"), lineWidth: 1)
)
.onTapGesture {
hideKeyboard()
AppState.shared.back()
}
Text("수정")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color(hex: "3BB9F1"))
.cornerRadius(10)
.onTapGesture {
hideKeyboard()
viewModel.modifyCommunityPost {
AppState.shared.back()
DispatchQueue.main.async {
onSuccess()
}
}
}
}
.padding(13.3)
.frame(maxWidth: .infinity)
.background(Color(hex: "222222"))
.cornerRadius(16.7, corners: [.topLeft, .topRight])
Rectangle()
.foregroundColor(Color(hex: "222222"))
.frame(height: keyboardHandler.keyboardHeight)
.frame(maxWidth: .infinity)
if proxy.safeAreaInsets.bottom > 0 {
Rectangle()
.foregroundColor(Color(hex: "222222"))
.frame(height: 15.3)
.frame(maxWidth: .infinity)
}
}
.padding(.top, 100)
}
}
}
if isShowPhotoPicker {
ImagePicker(
isShowing: $isShowPhotoPicker,
selectedImage: $viewModel.postImage,
sourceType: .photoLibrary
)
}
}
.onTapGesture { hideKeyboard() }
.edgesIgnoringSafeArea(.bottom)
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.onAppear {
viewModel.postId = postId
viewModel.getCommunityPostDetail {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
AppState.shared.back()
}
}
}
}
}
}
}
struct CreatorCommunityModifyView_Previews: PreviewProvider {
static var previews: some View {
CreatorCommunityModifyView(postId: 0) {}
}
}

View File

@ -0,0 +1,166 @@
//
// CreatorCommunityModifyViewModel.swift
// SodaLive
//
// Created by klaus on 2023/12/21.
//
import UIKit
import Moya
import Combine
final class CreatorCommunityModifyViewModel: ObservableObject {
private let repository = CreatorCommunityRepository()
private var subscription = Set<AnyCancellable>()
@Published var isLoading = false
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var content = ""
@Published var isAdult = false
@Published var isAvailableComment = true
@Published var postImage: UIImage? = nil
@Published var postImageUrl: String? = nil
@Published private(set) var communityPost: GetCommunityPostListResponse?
var placeholder = "내용을 입력하세요"
var postId = 0
func getCommunityPostDetail(onFailure: (() -> Void)? = nil) {
communityPost = nil
isLoading = true
repository.getCommunityPostDetail(postId: postId)
.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<GetCommunityPostListResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.communityPost = data
self.content = data.content
self.isAdult = data.isAdult
self.isAvailableComment = data.isCommentAvailable
self.postImageUrl = data.imageUrl
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
if let onFailure = onFailure {
onFailure()
}
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
if let onFailure = onFailure {
onFailure()
}
}
}
.store(in: &subscription)
}
func modifyCommunityPost(onSuccess: @escaping () -> Void) {
if !isLoading && validateData() {
isLoading = true
let request = ModifyCommunityPostRequest(
creatorCommunityId: postId,
content: communityPost!.content != content ? content : nil,
isCommentAvailable: communityPost!.isCommentAvailable != isAvailableComment ? isAvailableComment : nil,
isAdult: communityPost!.isAdult != isAdult ? isAdult : nil
)
var multipartData = [MultipartFormData]()
let encoder = JSONEncoder()
encoder.outputFormatting = .withoutEscapingSlashes
let jsonData = try? encoder.encode(request)
if let jsonData = jsonData {
if let postImage = postImage, let imageData = postImage.jpegData(compressionQuality: 0.8) {
multipartData.append(
MultipartFormData(
provider: .data(imageData),
name: "postImage",
fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg",
mimeType: "image/*")
)
}
multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request"))
repository
.modifyCommunityPost(parameters: multipartData)
.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 = "게시물이 수정되었습니다."
self.isShowPopup = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
onSuccess()
}
} 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)
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
self.isLoading = false
}
}
}
private func validateData() -> Bool {
if content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || content.count < 5 {
errorMessage = "내용을 5자 이상 입력해 주세요."
isShowPopup = true
return false
}
return true
}
}

View File

@ -93,7 +93,6 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
self.isShowPopup = true
self.isLoading = false
}
}
}