feat(image): 이미지 선택 후 크롭 편집 흐름을 적용한다
This commit is contained in:
@@ -6,15 +6,17 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import SDWebImageSwiftUI
|
||||
|
||||
struct CreatorCommunityWriteView: View {
|
||||
|
||||
@StateObject var keyboardHandler = KeyboardHandler()
|
||||
@StateObject private var viewModel = CreatorCommunityWriteViewModel()
|
||||
|
||||
@State private var selectedItem: PhotosPickerItem? = nil
|
||||
@State private var selectedPickedImage: UIImage?
|
||||
@State private var cropSourceImage: UIImage?
|
||||
@State private var isShowImageCropper = false
|
||||
@State private var isShowPhotoPicker = false
|
||||
@State private var isImageLoading = false
|
||||
|
||||
@State private var isShowRecordingVoiceView = false
|
||||
@State private var fileName: String = "녹음"
|
||||
@@ -38,53 +40,35 @@ struct CreatorCommunityWriteView: View {
|
||||
.foregroundColor(Color.grayee)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
PhotosPicker(
|
||||
selection: $selectedItem,
|
||||
matching: .any(of: [.images]),
|
||||
photoLibrary: .shared()) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
if let selectedImage = viewModel.postImageData {
|
||||
AnimatedImage(data: selectedImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: imagePreviewSize, height: imagePreviewSize)
|
||||
.cornerRadius(8)
|
||||
.clipped()
|
||||
} else {
|
||||
Image("ic_logo2")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.padding(13.3)
|
||||
.frame(width: imagePreviewSize, height: imagePreviewSize)
|
||||
.background(Color.bg)
|
||||
.cornerRadius(8)
|
||||
.clipped()
|
||||
}
|
||||
|
||||
Image("ic_camera")
|
||||
.padding(10)
|
||||
.background(Color.button)
|
||||
.cornerRadius(30)
|
||||
.offset(x: 15, y: 0)
|
||||
}
|
||||
.frame(width: imagePreviewSize, height: imagePreviewSize, alignment: .bottomTrailing)
|
||||
}
|
||||
.onChange(of: selectedItem) { newItem in
|
||||
Task {
|
||||
if let item = newItem {
|
||||
do {
|
||||
// ✅ 이미지 원본 Data 가져오기 (GIF 포함)
|
||||
if let data = try await item.loadTransferable(type: Data.self) {
|
||||
viewModel.postImageData = data
|
||||
}
|
||||
} catch {
|
||||
viewModel.errorMessage = "이미지를 로드하지 못했습니다."
|
||||
viewModel.isShowPopup = true
|
||||
DEBUG_LOG("이미지 로드 실패: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
if let selectedImage = viewModel.postImage {
|
||||
Image(uiImage: selectedImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: imagePreviewSize, height: imagePreviewSize)
|
||||
.cornerRadius(8)
|
||||
.clipped()
|
||||
} else {
|
||||
Image("ic_logo2")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.padding(13.3)
|
||||
.frame(width: imagePreviewSize, height: imagePreviewSize)
|
||||
.background(Color.bg)
|
||||
.cornerRadius(8)
|
||||
.clipped()
|
||||
}
|
||||
|
||||
Image("ic_camera")
|
||||
.padding(10)
|
||||
.background(Color.button)
|
||||
.cornerRadius(30)
|
||||
.offset(x: 15, y: 0)
|
||||
}
|
||||
.frame(width: imagePreviewSize, height: imagePreviewSize, alignment: .bottomTrailing)
|
||||
.onTapGesture {
|
||||
isShowPhotoPicker = true
|
||||
}
|
||||
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
Text("※ ")
|
||||
@@ -341,10 +325,64 @@ struct CreatorCommunityWriteView: View {
|
||||
soundData: $viewModel.soundData
|
||||
)
|
||||
}
|
||||
|
||||
if isShowPhotoPicker {
|
||||
ImagePicker(
|
||||
isShowing: $isShowPhotoPicker,
|
||||
selectedImage: $selectedPickedImage,
|
||||
sourceType: .photoLibrary
|
||||
)
|
||||
}
|
||||
|
||||
if isImageLoading {
|
||||
ZStack {
|
||||
Color.black.opacity(0.45)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { hideKeyboard() }
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onDisappear {
|
||||
isImageLoading = false
|
||||
}
|
||||
.onChange(of: selectedPickedImage, perform: { newImage in
|
||||
guard let newImage else {
|
||||
return
|
||||
}
|
||||
|
||||
isImageLoading = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let normalizedImage = newImage.normalizedForCrop()
|
||||
DispatchQueue.main.async {
|
||||
isImageLoading = false
|
||||
selectedPickedImage = nil
|
||||
cropSourceImage = normalizedImage
|
||||
isShowImageCropper = true
|
||||
}
|
||||
}
|
||||
})
|
||||
.sheet(isPresented: $isShowImageCropper, onDismiss: {
|
||||
cropSourceImage = nil
|
||||
}) {
|
||||
if let cropSourceImage {
|
||||
ImageCropEditorView(
|
||||
image: cropSourceImage,
|
||||
aspectPolicy: .free,
|
||||
onCancel: {
|
||||
isShowImageCropper = false
|
||||
},
|
||||
onComplete: { croppedImage in
|
||||
viewModel.setPostImage(croppedImage)
|
||||
isShowImageCropper = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,7 +390,9 @@ struct CreatorCommunityWriteView: View {
|
||||
private func deleteAudioFile() {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: getAudioFileURL())
|
||||
} catch {}
|
||||
} catch {
|
||||
DEBUG_LOG("오디오 파일 삭제 실패: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func getAudioFileURL() -> URL {
|
||||
|
||||
@@ -29,6 +29,7 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
|
||||
}
|
||||
@Published var isAvailableComment = true
|
||||
@Published var postImageData: Data? = nil
|
||||
@Published var postImage: UIImage? = nil
|
||||
|
||||
@Published var priceString = "0" {
|
||||
didSet {
|
||||
@@ -47,6 +48,11 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
|
||||
@Published var soundData: Data? = nil
|
||||
|
||||
var placeholder = "내용을 입력하세요"
|
||||
|
||||
func setPostImage(_ image: UIImage) {
|
||||
postImage = image
|
||||
postImageData = image.jpegData(compressionQuality: 0.8)
|
||||
}
|
||||
|
||||
func createCommunityPost(onSuccess: @escaping () -> Void) {
|
||||
if !isLoading && validateData() {
|
||||
@@ -65,8 +71,8 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
|
||||
MultipartFormData(
|
||||
provider: .data(postImageData),
|
||||
name: "postImage",
|
||||
fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000)",
|
||||
mimeType: "image/*"
|
||||
fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg",
|
||||
mimeType: "image/jpeg"
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -92,6 +98,9 @@ final class CreatorCommunityWriteViewModel: ObservableObject {
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
self.isLoading = false
|
||||
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} receiveValue: { [unowned self] response in
|
||||
self.isLoading = false
|
||||
|
||||
Reference in New Issue
Block a user