diff --git a/JUDA.xcodeproj/project.pbxproj b/JUDA.xcodeproj/project.pbxproj index 27eaa9a4..28c056da 100644 --- a/JUDA.xcodeproj/project.pbxproj +++ b/JUDA.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ 2C63DD612B67D5EB00770E3A /* PhotoSelectPagingTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C63DD602B67D5EB00770E3A /* PhotoSelectPagingTab.swift */; }; 2C63DD652B67DFA400770E3A /* DrinkTagScroll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C63DD642B67DFA400770E3A /* DrinkTagScroll.swift */; }; 2C63DD672B67EB1B00770E3A /* RecordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C63DD662B67EB1B00770E3A /* RecordView.swift */; }; + 2C69CDAC2B9DF61600F70F80 /* RecordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C69CDAB2B9DF61600F70F80 /* RecordViewModel.swift */; }; 2C7FD5522B848EEF00169512 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7FD5512B848EEF00169512 /* Post.swift */; }; 2CACB53C2B99BAC500DD6DCE /* FireStorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CACB53B2B99BAC500DD6DCE /* FireStorageService.swift */; }; 7B0A8BC52B8C163E00740E61 /* Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0A8BC42B8C163E00740E61 /* Report.swift */; }; @@ -100,7 +101,6 @@ 7B6D76B52B67D5F900601B55 /* PostTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D76B42B67D5F900601B55 /* PostTags.swift */; }; 7B6D76B92B68D9C900601B55 /* PostReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D76B82B68D9C900601B55 /* PostReportView.swift */; }; 7B74FA5B2B9A9EE800BAC2AC /* PostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B74FA5A2B9A9EE800BAC2AC /* PostViewModel.swift */; }; - 7B74FA5D2B9ABF7600BAC2AC /* RecordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B74FA5C2B9ABF7600BAC2AC /* RecordViewModel.swift */; }; 7B881CA32B97464200571352 /* FirestorePostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B881CA22B97464200571352 /* FirestorePostService.swift */; }; 7B881CA52B97545700571352 /* FirestoreDrinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B881CA42B97545700571352 /* FirestoreDrinkService.swift */; }; 7B904C662B6168C60052384A /* PostCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B904C652B6168C60052384A /* PostCell.swift */; }; @@ -174,7 +174,7 @@ B1EE24922B61447F007F68B0 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE24912B61447F007F68B0 /* MainView.swift */; }; B1EE24942B614485007F68B0 /* LogInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE24932B614485007F68B0 /* LogInView.swift */; }; B1EE24982B6144BD007F68B0 /* ExLikedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE24972B6144BD007F68B0 /* ExLikedViewModel.swift */; }; - B1EE249A2B6144C5007F68B0 /* RecordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE24992B6144C5007F68B0 /* RecordViewModel.swift */; }; + B1EE249A2B6144C5007F68B0 /* ExRecordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE24992B6144C5007F68B0 /* ExRecordViewModel.swift */; }; B1EE249C2B6144CD007F68B0 /* ExPostsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE249B2B6144CD007F68B0 /* ExPostsViewModel.swift */; }; B1EE249E2B6144DD007F68B0 /* ExDrinkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE249D2B6144DD007F68B0 /* ExDrinkViewModel.swift */; }; B1EE24A52B614872007F68B0 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EE24A42B614872007F68B0 /* SearchBar.swift */; }; @@ -255,6 +255,7 @@ 2C63DD602B67D5EB00770E3A /* PhotoSelectPagingTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSelectPagingTab.swift; sourceTree = ""; }; 2C63DD642B67DFA400770E3A /* DrinkTagScroll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrinkTagScroll.swift; sourceTree = ""; }; 2C63DD662B67EB1B00770E3A /* RecordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordView.swift; sourceTree = ""; }; + 2C69CDAB2B9DF61600F70F80 /* RecordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordViewModel.swift; sourceTree = ""; }; 2C7FD5512B848EEF00169512 /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; 2CACB53B2B99BAC500DD6DCE /* FireStorageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireStorageService.swift; sourceTree = ""; }; 7B0A8BC42B8C163E00740E61 /* Report.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Report.swift; sourceTree = ""; }; @@ -272,7 +273,6 @@ 7B6D76B62B68972000601B55 /* PostGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostGrid.swift; sourceTree = ""; }; 7B6D76B82B68D9C900601B55 /* PostReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostReportView.swift; sourceTree = ""; }; 7B74FA5A2B9A9EE800BAC2AC /* PostViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewModel.swift; sourceTree = ""; }; - 7B74FA5C2B9ABF7600BAC2AC /* RecordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordViewModel.swift; sourceTree = ""; }; 7B881CA22B97464200571352 /* FirestorePostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestorePostService.swift; sourceTree = ""; }; 7B881CA42B97545700571352 /* FirestoreDrinkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreDrinkService.swift; sourceTree = ""; }; 7B904C652B6168C60052384A /* PostCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCell.swift; sourceTree = ""; }; @@ -323,7 +323,7 @@ B1EE24912B61447F007F68B0 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; B1EE24932B614485007F68B0 /* LogInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogInView.swift; sourceTree = ""; }; B1EE24972B6144BD007F68B0 /* ExLikedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExLikedViewModel.swift; sourceTree = ""; }; - B1EE24992B6144C5007F68B0 /* RecordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordViewModel.swift; sourceTree = ""; }; + B1EE24992B6144C5007F68B0 /* ExRecordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExRecordViewModel.swift; sourceTree = ""; }; B1EE249B2B6144CD007F68B0 /* ExPostsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExPostsViewModel.swift; sourceTree = ""; }; B1EE249D2B6144DD007F68B0 /* ExDrinkViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExDrinkViewModel.swift; sourceTree = ""; }; B1EE24A42B614872007F68B0 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; @@ -727,7 +727,8 @@ B1EE24822B6143D2007F68B0 /* Record */ = { isa = PBXGroup; children = ( - B1EE24992B6144C5007F68B0 /* RecordViewModel.swift */, + B1EE24992B6144C5007F68B0 /* ExRecordViewModel.swift */, + 2C69CDAB2B9DF61600F70F80 /* RecordViewModel.swift */, ); path = Record; sourceTree = ""; @@ -740,7 +741,6 @@ 7B881CA22B97464200571352 /* FirestorePostService.swift */, 7B74FA5A2B9A9EE800BAC2AC /* PostViewModel.swift */, 096F2D5C2B9C583E00DFC5A3 /* FirestoreReportService.swift */, - 7B74FA5C2B9ABF7600BAC2AC /* RecordViewModel.swift */, ); path = Posts; sourceTree = ""; @@ -921,7 +921,6 @@ 7BBDFF6F2B6B90EB0036FB8B /* AddTagView.swift in Sources */, 09A320162B8B30D100DE4646 /* ExSearchDrinkViewModel.swift in Sources */, 7BBDFF702B6B90EB0036FB8B /* PostGrid.swift in Sources */, - 7B74FA5D2B9ABF7600BAC2AC /* RecordViewModel.swift in Sources */, 7B881CA52B97545700571352 /* FirestoreDrinkService.swift in Sources */, 090FF1CE2B83413100AF22B5 /* CustomLoadingView.swift in Sources */, 2C5B0F422B6B10CF005F1A99 /* CustomDialog.swift in Sources */, @@ -946,7 +945,7 @@ 7B881CA32B97464200571352 /* FirestorePostService.swift in Sources */, 0974A1502B8B7B6C00476199 /* ShimmerPostCell.swift in Sources */, 09F869B52B6A47B700A56A4C /* AlarmStoreListCell.swift in Sources */, - B1EE249A2B6144C5007F68B0 /* RecordViewModel.swift in Sources */, + B1EE249A2B6144C5007F68B0 /* ExRecordViewModel.swift in Sources */, 2C63DD672B67EB1B00770E3A /* RecordView.swift in Sources */, AC797BDA2B8B8BEB0018D227 /* DrinkTopView.swift in Sources */, 7B6D76AF2B67CFC900601B55 /* PostInfo.swift in Sources */, @@ -1003,6 +1002,7 @@ 096F2D5B2B9B044400DFC5A3 /* DrinkViewModel.swift in Sources */, 099912652B85799800E72C73 /* CircularLoaderView.swift in Sources */, 09BCC1582B7C805400FF4D1B /* UserAgreementView.swift in Sources */, + 2C69CDAC2B9DF61600F70F80 /* RecordViewModel.swift in Sources */, AC7EA1D92B8D77EE00E3E5EC /* Bundle +.swift in Sources */, B121A5722B5F8EE100667D42 /* Font +.swift in Sources */, B1EE249C2B6144CD007F68B0 /* ExPostsViewModel.swift in Sources */, diff --git a/JUDA/Model/Drink.swift b/JUDA/Model/Drink.swift index 3375f1da..2021bed2 100644 --- a/JUDA/Model/Drink.swift +++ b/JUDA/Model/Drink.swift @@ -23,7 +23,7 @@ struct Drink { let drinkField: DrinkField let taggedPosts: [Post] let agePreference: AgePreference - let GenderPreference: GenderPreference + let genderPreference: GenderPreference let likedUsersID: [String] } diff --git a/JUDA/Model/Post.swift b/JUDA/Model/Post.swift index 2c24ad4c..f3e8e2df 100644 --- a/JUDA/Model/Post.swift +++ b/JUDA/Model/Post.swift @@ -29,6 +29,8 @@ struct PostField: Codable { struct WrittenUser: Codable { var userID: String var userName: String + var userAge: Int + var userGender: String var userProfileImageURL: URL } diff --git a/JUDA/ViewModel/App/FireStorageService.swift b/JUDA/ViewModel/App/FireStorageService.swift index 54910344..5fb23276 100644 --- a/JUDA/ViewModel/App/FireStorageService.swift +++ b/JUDA/ViewModel/App/FireStorageService.swift @@ -22,9 +22,13 @@ final class FireStorageService { private let imageType = "image/jpg" // FireStorage 에 이미지 올리기 - func uploadImageToStorage(folder: FireStorageFolderType, image: UIImage, fileName: String) async throws { + func uploadImageToStorage(folder: FireStorageFolderType, userID: String? = nil, postID: String? = nil, image: UIImage, fileName: String) async throws { do { - let storageRoute = storageRef.child("\(folder.rawValue)/\(fileName)") + var storagePath = folder.rawValue + if let userID = userID, let postID = postID { + storagePath += "/\(userID)/\(postID)" + } + let storageRoute = storageRef.child("\(storagePath)/\(fileName)") let data = Formatter.compressImage(image) let metaData = StorageMetadata() metaData.contentType = imageType diff --git a/JUDA/ViewModel/Drink/FirestoreDrinkService.swift b/JUDA/ViewModel/Drink/FirestoreDrinkService.swift index a2a94fa4..d88f1e95 100644 --- a/JUDA/ViewModel/Drink/FirestoreDrinkService.swift +++ b/JUDA/ViewModel/Drink/FirestoreDrinkService.swift @@ -69,7 +69,7 @@ extension FirestoreDrinkService { return Drink(drinkField: drikField, taggedPosts: taggedPosts, agePreference: agePreference, - GenderPreference: genderPreference, + genderPreference: genderPreference, likedUsersID: likedUsersID) } catch DrinkError.fetchDrinkField { print("error :: fetchDrinkField() -> fetch drink field data failure") @@ -196,6 +196,26 @@ extension FirestoreDrinkService { } } +extension FirestoreDrinkService { + // drink collection agePreference data update 메서드 + func updateDrinkAgePreference(ref: CollectionReference, drinkID: String, age: String, userID: String) async { + do { + try await ref.document(drinkID).collection("agePreference").document(age).collection(age).document(userID).setData([:]) + } catch { + // TODO: error 처리 + } + } + + // drink collection genderPreference data update 메서드 + func updateDrinkGenderPreference(ref: CollectionReference, drinkID: String, gender: String, userID: String) async { + do { + try await ref.document(drinkID).collection("genderPreference").document(gender).collection(gender).document(userID).setData([:]) + } catch { + // TODO: error 처리 + } + } +} + // MARK: Firestore drink document delete extension FirestoreDrinkService { // drinks collection에서 삭제하고싶은 drink에 해당하는 document 삭제 메서드 diff --git a/JUDA/ViewModel/Posts/FirestorePostService.swift b/JUDA/ViewModel/Posts/FirestorePostService.swift index c2bf205c..698894ad 100644 --- a/JUDA/ViewModel/Posts/FirestorePostService.swift +++ b/JUDA/ViewModel/Posts/FirestorePostService.swift @@ -93,8 +93,7 @@ extension FirestorePostService { //MARK: Firestore post document upload extension FirestorePostService { - func uploadPostDocument(post: Post) async throws { - let postID = UUID().uuidString + func uploadPostDocument(post: Post, postID: String) async throws { let postDocumentRef = Firestore.firestore().collection("posts").document(postID) let likedUsersIDCollectionRef = postDocumentRef.collection("likedUsersID") diff --git a/JUDA/ViewModel/Posts/RecordViewModel.swift b/JUDA/ViewModel/Posts/RecordViewModel.swift deleted file mode 100644 index 2ad4784b..00000000 --- a/JUDA/ViewModel/Posts/RecordViewModel.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// RecordService.swift -// JUDA -// -// Created by Minjae Kim on 3/8/24. -// - -import Foundation diff --git a/JUDA/ViewModel/Record/ExRecordViewModel.swift b/JUDA/ViewModel/Record/ExRecordViewModel.swift new file mode 100644 index 00000000..8633f842 --- /dev/null +++ b/JUDA/ViewModel/Record/ExRecordViewModel.swift @@ -0,0 +1,220 @@ +// +// RecordViewModel.swift +// JUDA +// +// Created by 홍세희 on 2024/01/24. +// + +import SwiftUI +import FirebaseCore +import FirebaseFirestore +import FirebaseStorage +import PhotosUI + +final class ExRecordViewModel: ObservableObject { + // post 업로드용 Post 모델 객체 + @Published var post: Post? + // (dirnkID, (drinkData, rating)) 튜플 형태의 선택한 술 Data + @Published var selectedDrinkTag: DrinkTag? + // [drinkID: (drinkData, rating)] 딕셔너리 형태의 태그된 모든 술 Data + @Published var drinkTags: [DrinkTag] = [] + // 라이브러리에서 선택된 모든 사진 Data + @Published var images: [UIImage] = [] + // 선택된 모든 사진 Data의 ID를 갖는 배열 + var imagesURL: [URL] = [] + // 글 내용을 담는 프로퍼티 + @Published var content: String = "" + // 음식 태그를 담는 배열 + @Published var foodTags: [String] = [] + // 화면 너비 받아오기 + var windowWidth: CGFloat { + TagHandler.getScreenWidthWithoutPadding(padding: 20) + } + // post 업로드 완료 확인 및 로딩 뷰 출력용 프로퍼티 + @Published var isPostUploadSuccess = false + + // Firestore connection + private let db = Firestore.firestore() + + // FireStorage 기본 경로 + private let storage = Storage.storage() + private let drinkImagesPath = "drinkImages/" +} + +// MARK: - FirebaseStorage Image Upload +extension ExRecordViewModel { + func uploadMultipleImagesToFirebaseStorageAsync(_ imagesData: [Data]) async throws { + // 여러 이미지 업로드를 동시에 처리하기 위한 비동기 작업 배열 + var uploadTasks: [Task<(Int, URL), Error>] = [] + + // 각 이미지 데이터에 대해 비동기 업로드 작업 생성 및 배열에 추가 + for (index, imageData) in imagesData.enumerated() { + let uploadTask = Task { try await uploadImageToFirebaseStorageAsync(imageData, index: index) } + uploadTasks.append(uploadTask) + } + + // 모든 업로드 작업이 완료될 때까지 기다린 후 결과 URL 배열 반환 + return try await withThrowingTaskGroup(of: (Int, URL).self, body: { group in + var downloadURLs: [(Int, URL)] = [] + + // 각 업로드 작업을 TaskGroup에 추가 + for task in uploadTasks { + group.addTask { + // Task의 결과를 반환 + try await task.value + } + } + + // TaskGroup의 모든 결과를 수집 + for try await downloadURL in group { + downloadURLs.append(downloadURL) + } + + downloadURLs.sort(by: { $0.0 < $1.0 }) + self.imagesURL = downloadURLs.map { $0.1 } + }) + } + + func uploadImageToFirebaseStorageAsync(_ imageData: Data, index: Int) async throws -> (Int, URL) { + let storageRef = Storage.storage().reference() + let imageID = UUID().uuidString // 고유한 이미지 ID 생성 + let imageRef = storageRef.child("postImages/\(imageID).jpg") + + let metadata = StorageMetadata() + metadata.contentType = "image/jpg" + + // 이미지 업로드 + let _ = try await imageRef.putDataAsync(imageData, metadata: metadata) + + // 업로드된 이미지의 URL 가져오기 + let downloadURL = try await imageRef.downloadURL() + + return (index, downloadURL) + } +} + +// MARK: - Firestore Post Upload & Drink Update +extension ExRecordViewModel { + // Firestore post data upload + func uploadPost() async { + guard let post = post, let userID = post.userField.userID else { + print("uploadPost() :: error -> don't get post & post's userID") + return + } + // posts documentID uuid 지정 + let postDocumentPath = UUID().uuidString + let postRef = db.collection("posts") + let userPostRef = db.collection("users").document(userID).collection("posts") +// await firebaseUploadPost(ref: userPostRef, documentPath: postDocumentPath) + let references: [CollectionReference] = [postRef, userPostRef] + + // 동일한 post collection data를 갖는 collection(posts, users/posts)에 data upload + for reference in references { + await firebaseUploadPost(ref: reference, documentPath: postDocumentPath) + } + + // 동일한 drink collection data를 갖는 collection(posts/drinkTags/drink, drinks)에 data update + await updateDrinkDataWithTag(documentPath: postDocumentPath, userID: userID) + } + + // Firestore posts collection upload + func firebaseUploadPost(ref: CollectionReference, documentPath: String) async { + guard let post = post else { return } + do { + // posts collection field data upload + try ref.document(documentPath).setData(from: post.postField) + // drinkTags collection data upload in posts collection + for drinkTag in drinkTags { + guard let drinkID = drinkTag.drink.drinkID else { + print("firebaseUploadPost() :: error -> don't get drinkID") + continue + } + try await ref.document(documentPath).collection("drinkTags").document(drinkID).setData(["rating": drinkTag.rating]) + try ref.document(documentPath).collection("drinkTags").document(drinkID).collection("drink").document(drinkID).setData(from: drinkTag.drink) + } + + // user collection data upload in posts collection + try ref.document(documentPath) + .collection("user") + .document(post.userField.userID ?? "") + .setData(from: post.userField) + + } catch { + print("error :: post upload fail") + } + } + + // claculate rating data when user upload post with drinkTag + func calcDrinkRating(prev: Double, new: Double, count: Int) -> Double { + return (prev * Double(count) + new) / (Double(count) + 1) + } + + // Firestore drink collection update + func updateDrinkDataWithTag(documentPath: String, userID: String) async { + // drinks + let drinkRef = db.collection("drinks") + // posts/drinkTags/drink + let drinkTagRef = db.collection("posts").document(documentPath).collection("drinkTags") + // users/posts/drinkTags/drink + let userPostDrinkTagRef = db.collection("users").document(userID).collection("posts").document(documentPath).collection("drinkTags") + + do { + for drinkTag in drinkTags { + guard let post = post, let drinkID = drinkTag.drink.drinkID else { return } + var updateData: [String: Any] = [:] + // drink 정보를 바탕으로 update + let drinkData = try await drinkRef.document(drinkID).getDocument(as: FBDrink.self) + // rating이 4보다 큰 경우 + // agePreference, genderPreference + if drinkTag.rating >= 4 { + let userAge: Int = post.userField.age / 10 * 10 + let stringUserAge = String(userAge < 20 ? 20 : userAge) + let userGender = post.userField.gender + // agePreference + 1 + var agePreference = drinkData.agePreference + agePreference[stringUserAge] = (agePreference[stringUserAge] ?? 0) + 1 + // genderPreference + 1 + var genderPreference = drinkData.genderPreference + genderPreference[userGender] = (genderPreference[userGender] ?? 0) + 1 + + updateData["agePreference"] = agePreference + updateData["genderPreference"] = genderPreference + } + // rating + let prev = drinkData.rating + let new = drinkTag.rating + let count = drinkData.taggedPostID.count + let rating = calcDrinkRating(prev: prev, new: new, count: count) + + // taggedPostID + var taggedPostID = drinkData.taggedPostID + taggedPostID.append(documentPath) + + updateData["rating"] = rating + updateData["taggedPostID"] = taggedPostID + + // drink data(agePreference, genderPreference, rating, taggedPostId) update in drinks, posts collection(posts/drinkTags) + try await drinkRef.document(drinkID).updateData(updateData) + try await drinkTagRef.document(drinkID).collection("drink").document(drinkID).updateData(updateData) + try await userPostDrinkTagRef.document(drinkID).collection("drink").document(drinkID).updateData(updateData) + } + } catch { + print("update error") + } + } + + func recordPostDataClear() { + self.post = nil + self.selectedDrinkTag = nil + self.drinkTags = [] + self.images = [] + self.imagesURL = [] + self.content = "" + self.foodTags = [] + } +} + +// MARK: Post Modify +extension ExRecordViewModel { + +} diff --git a/JUDA/ViewModel/Record/RecordViewModel.swift b/JUDA/ViewModel/Record/RecordViewModel.swift index d7141d59..1c0f7c00 100644 --- a/JUDA/ViewModel/Record/RecordViewModel.swift +++ b/JUDA/ViewModel/Record/RecordViewModel.swift @@ -2,219 +2,153 @@ // RecordViewModel.swift // JUDA // -// Created by 홍세희 on 2024/01/24. +// Created by 정인선 on 3/10/24. // import SwiftUI -import FirebaseCore import FirebaseFirestore -import FirebaseStorage -import PhotosUI -final class RecordViewModel: ObservableObject { - // post 업로드용 Post 모델 객체 +@MainActor +final class RecordViewModel { + // post 업로드용 Post 객체 @Published var post: Post? - // (dirnkID, (drinkData, rating)) 튜플 형태의 선택한 술 Data - @Published var selectedDrinkTag: DrinkTag? - // [drinkID: (drinkData, rating)] 딕셔너리 형태의 태그된 모든 술 Data - @Published var drinkTags: [DrinkTag] = [] + // post 작성자 정보를 갖는 WrittenUser 객체 + @Published var user: WrittenUser? + // post의 전체 술 평가 정보를 갖는 DrinkTag 배열 + @Published var drinkTags = [DrinkTag]() // 라이브러리에서 선택된 모든 사진 Data - @Published var images: [UIImage] = [] - // 선택된 모든 사진 Data의 ID를 갖는 배열 - var imagesURL: [URL] = [] + @Published var images = [UIImage]() // 글 내용을 담는 프로퍼티 @Published var content: String = "" // 음식 태그를 담는 배열 - @Published var foodTags: [String] = [] - // 화면 너비 받아오기 - var windowWidth: CGFloat { - TagHandler.getScreenWidthWithoutPadding(padding: 20) - } - // post 업로드 완료 확인 및 로딩 뷰 출력용 프로퍼티 - @Published var isPostUploadSuccess = false + @Published var foodTags = [String]() - // Firestore connection - private let db = Firestore.firestore() - - // FireStorage 기본 경로 - private let storage = Storage.storage() - private let drinkImagesPath = "drinkImages/" -} - -// MARK: - FirebaseStorage Image Upload -extension RecordViewModel { - func uploadMultipleImagesToFirebaseStorageAsync(_ imagesData: [Data]) async throws { - // 여러 이미지 업로드를 동시에 처리하기 위한 비동기 작업 배열 - var uploadTasks: [Task<(Int, URL), Error>] = [] - - // 각 이미지 데이터에 대해 비동기 업로드 작업 생성 및 배열에 추가 - for (index, imageData) in imagesData.enumerated() { - let uploadTask = Task { try await uploadImageToFirebaseStorageAsync(imageData, index: index) } - uploadTasks.append(uploadTask) - } - - // 모든 업로드 작업이 완료될 때까지 기다린 후 결과 URL 배열 반환 - return try await withThrowingTaskGroup(of: (Int, URL).self, body: { group in - var downloadURLs: [(Int, URL)] = [] - - // 각 업로드 작업을 TaskGroup에 추가 - for task in uploadTasks { - group.addTask { - // Task의 결과를 반환 - try await task.value - } - } - - // TaskGroup의 모든 결과를 수집 - for try await downloadURL in group { - downloadURLs.append(downloadURL) - } + // post 업로드, iamges 업로드를 위한 postID + private var postID = "" + // 모든 imagesURL을 갖는 배열 + private var imagesURL = [URL]() - downloadURLs.sort(by: { $0.0 < $1.0 }) - self.imagesURL = downloadURLs.map { $0.1 } - }) - } - - func uploadImageToFirebaseStorageAsync(_ imageData: Data, index: Int) async throws -> (Int, URL) { - let storageRef = Storage.storage().reference() - let imageID = UUID().uuidString // 고유한 이미지 ID 생성 - let imageRef = storageRef.child("postImages/\(imageID).jpg") - - let metadata = StorageMetadata() - metadata.contentType = "image/jpg" - - // 이미지 업로드 - let _ = try await imageRef.putDataAsync(imageData, metadata: metadata) - - // 업로드된 이미지의 URL 가져오기 - let downloadURL = try await imageRef.downloadURL() - - return (index, downloadURL) - } -} - -// MARK: - Firestore Post Upload & Drink Update -extension RecordViewModel { - // Firestore post data upload - func uploadPost() async { - guard let post = post, let userID = post.userField.userID else { - print("uploadPost() :: error -> don't get post & post's userID") - return - } - // posts documentID uuid 지정 - let postDocumentPath = UUID().uuidString - let postRef = db.collection("posts") - let userPostRef = db.collection("users").document(userID).collection("posts") -// await firebaseUploadPost(ref: userPostRef, documentPath: postDocumentPath) - let references: [CollectionReference] = [postRef, userPostRef] - - // 동일한 post collection data를 갖는 collection(posts, users/posts)에 data upload - for reference in references { - await firebaseUploadPost(ref: reference, documentPath: postDocumentPath) - } - - // 동일한 drink collection data를 갖는 collection(posts/drinkTags/drink, drinks)에 data update - await updateDrinkDataWithTag(documentPath: postDocumentPath, userID: userID) - } + // firebase Post Service + private let firestorePostService = FirestorePostService() + // Firebase Drink Service + private let firestoreDrinkService = FirestoreDrinkService() + // FireStorage Service + private let fireStorageService = FireStorageService() - // Firestore posts collection upload - func firebaseUploadPost(ref: CollectionReference, documentPath: String) async { - guard let post = post else { return } + // Firestore db 연결 + private let db = Firestore.firestore() + + // MARK: - FireStroage post 이미지 업로드 및 이미지 URL 받아오기 + func uploadMultipleImagesToFirebaseStorageAsync() async { + guard let user = user else { return } do { - // posts collection field data upload - try ref.document(documentPath).setData(from: post.postField) - // drinkTags collection data upload in posts collection - for drinkTag in drinkTags { - guard let drinkID = drinkTag.drink.drinkID else { - print("firebaseUploadPost() :: error -> don't get drinkID") - continue - } - try await ref.document(documentPath).collection("drinkTags").document(drinkID).setData(["rating": drinkTag.rating]) - try ref.document(documentPath).collection("drinkTags").document(drinkID).collection("drink").document(drinkID).setData(from: drinkTag.drink) - } + // 결과를 받을 배열 생성 + var downloadURLs: [(Int, URL)] = [] + + // 이미지 업로드 병렬처리를 위한 taskGroup + try await withThrowingTaskGroup(of: (Int, URL).self) { group in + for (index, image) in images.enumerated() { + // 각 이미지 데이터에 대해 비동기 업로드 작업 실행 및 배열에 추가 + group.addTask { + // storage 폴더링을 위한 userID + let userID = user.userID + // image fileName 생성 + let imageID = UUID().uuidString + // storage에 이미지 업로드 + try await self.fireStorageService.uploadImageToStorage(folder: .post, userID: userID , postID: self.postID, image: image, fileName: imageID) + // storage에서 이미지 URL 받아오기 + let imageURL = try await self.fireStorageService.fetchImageURL(folder: .post, fileName: imageID) + // (이미지 순서, URL) 반환 + return (index, imageURL) + } + } + + // task 반환값을 결과 배열에 저장 + for try await downloadURL in group { + downloadURLs.append(downloadURL) + } + // post의 imagesURL에 index순으로 정렬된 URL 배열 저장 + imagesURL = downloadURLs.sorted(by: { $0.0 < $1.0 }).map { $0.1 } + } + } catch FireStorageError.uploadImage { - // user collection data upload in posts collection - try ref.document(documentPath) - .collection("user") - .document(post.userField.userID ?? "") - .setData(from: post.userField) + } catch FireStorageError.fetchImageURL { } catch { - print("error :: post upload fail") + } } - // claculate rating data when user upload post with drinkTag - func calcDrinkRating(prev: Double, new: Double, count: Int) -> Double { - return (prev * Double(count) + new) / (Double(count) + 1) + // MARK: - Firestore post 업로드 + func uploadPost() async { + guard let post = post else { return } + do { + try await firestorePostService.uploadPostDocument(post: post, postID: postID) + } catch PostError.upload { + // TODO: error 처리 + } catch { + // TODO: error 처리 + } } - // Firestore drink collection update - func updateDrinkDataWithTag(documentPath: String, userID: String) async { - // drinks - let drinkRef = db.collection("drinks") - // posts/drinkTags/drink - let drinkTagRef = db.collection("posts").document(documentPath).collection("drinkTags") - // users/posts/drinkTags/drink - let userPostDrinkTagRef = db.collection("users").document(userID).collection("posts").document(documentPath).collection("drinkTags") - + // MARK: - Firestore drink 업로드 + func updateDrink() async { + guard let post = post, let user = user else { return } do { + let drinkRef = db.collection("drinks") for drinkTag in drinkTags { - guard let post = post, let drinkID = drinkTag.drink.drinkID else { return } - var updateData: [String: Any] = [:] - // drink 정보를 바탕으로 update - let drinkData = try await drinkRef.document(drinkID).getDocument(as: FBDrink.self) - // rating이 4보다 큰 경우 - // agePreference, genderPreference - if drinkTag.rating >= 4 { - let userAge: Int = post.userField.age / 10 * 10 - let stringUserAge = String(userAge < 20 ? 20 : userAge) - let userGender = post.userField.gender - // agePreference + 1 - var agePreference = drinkData.agePreference - agePreference[stringUserAge] = (agePreference[stringUserAge] ?? 0) + 1 - // genderPreference + 1 - var genderPreference = drinkData.genderPreference - genderPreference[userGender] = (genderPreference[userGender] ?? 0) + 1 + let drinkID = drinkTag.drinkID + let drinkRoute = drinkRef.document(drinkID) + // 해당 술 정보 가져오기 + let drinkData = try await firestoreDrinkService.fetchDrinkDocument(document: drinkRoute) + + // post 내 술 평가가 4점 이상일 때 agePreference, genderPreference update + if drinkTag.drinkRating >= 4 { + let userID = user.userID + let userAge = (user.userAge / 10) * 10 + // 20 이하인 경우 20으로, 50 이상인 경우 50으로 처리 + let stringUserAge = String(userAge < 20 ? 20 : (userAge > 50 ? 50 : userAge)) + let userGender = user.userGender - updateData["agePreference"] = agePreference - updateData["genderPreference"] = genderPreference - } - // rating - let prev = drinkData.rating - let new = drinkTag.rating - let count = drinkData.taggedPostID.count - let rating = calcDrinkRating(prev: prev, new: new, count: count) - - // taggedPostID - var taggedPostID = drinkData.taggedPostID - taggedPostID.append(documentPath) - - updateData["rating"] = rating - updateData["taggedPostID"] = taggedPostID - - // drink data(agePreference, genderPreference, rating, taggedPostId) update in drinks, posts collection(posts/drinkTags) - try await drinkRef.document(drinkID).updateData(updateData) - try await drinkTagRef.document(drinkID).collection("drink").document(drinkID).updateData(updateData) - try await userPostDrinkTagRef.document(drinkID).collection("drink").document(drinkID).updateData(updateData) + // Drink agePreference/userAge 에 userID 추가 + await firestoreDrinkService.updateDrinkAgePreference(ref: drinkRef, drinkID: drinkID, age: stringUserAge, userID: userID) + // Drink genderPreference/userGender 에 userID 추가 + await firestoreDrinkService.updateDrinkGenderPreference(ref: drinkRef, drinkID: drinkID, gender: userGender, userID: userID) + } + // rating + let prev = drinkData.drinkField.rating + let new = drinkTag.drinkRating + let count = drinkData.taggedPosts.count + let rating = calcDrinkRating(prev: prev, new: new, count: count) + + // 이거 왜 return Bool 주는지...? + // Drink rating update + let result = await firestoreDrinkService.updateDrinkField(ref: drinkRef, drinkID: drinkID, data: ["rating": rating]) } + } catch DrinkError.fetchDrinkDocument { + // TODO: error 처리 } catch { - print("update error") + // TODO: error 처리 } } - - func recordPostDataClear() { - self.post = nil - self.selectedDrinkTag = nil - self.drinkTags = [] - self.images = [] - self.imagesURL = [] - self.content = "" - self.foodTags = [] - } -} - -// MARK: Post Modify -extension RecordViewModel { - + + // 기존 rating을 받아서 새로 계산 + func calcDrinkRating(prev: Double, new: Double, count: Int) -> Double { + return (prev * Double(count) + new) / (Double(count) + 1) + } + + // MARK: - postID 생성 + func createPostID() { + postID = UUID().uuidString + } + + // MARK: - post Data 초기화 + func recordPostDataClear() { + post = nil + drinkTags = [] + images = [] + content = "" + foodTags = [] + postID = "" + } }