diff --git a/HappyAnding/HappyAnding.xcodeproj/project.pbxproj b/HappyAnding/HappyAnding.xcodeproj/project.pbxproj index d0d284bb..4ed19c1a 100644 --- a/HappyAnding/HappyAnding.xcodeproj/project.pbxproj +++ b/HappyAnding/HappyAnding.xcodeproj/project.pbxproj @@ -139,6 +139,14 @@ A3FF01882918581E00384211 /* LicenseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3FF01872918581E00384211 /* LicenseView.swift */; }; A3FF018A2918F8EF00384211 /* apache.txt in Resources */ = {isa = PBXBuildFile; fileRef = A3FF01892918F8EF00384211 /* apache.txt */; }; A3FF018E291ACFA500384211 /* WithdrawalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3FF018D291ACFA500384211 /* WithdrawalView.swift */; }; + F86DC3902BBC74CB00926285 /* CommunityRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86DC38F2BBC74CB00926285 /* CommunityRepository.swift */; }; + F86DC3922BBDB7E800926285 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86DC3912BBDB7E800926285 /* Post.swift */; }; + F86DC3942BBDB7F200926285 /* Answer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86DC3932BBDB7F200926285 /* Answer.swift */; }; + F86DC3962BBDBECF00926285 /* CommunityComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86DC3952BBDBECF00926285 /* CommunityComment.swift */; }; + F86DC3982BBDE17C00926285 /* CommunityRepositoryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86DC3972BBDE17C00926285 /* CommunityRepositoryTest.swift */; }; + F86DC39B2BBE7A0600926285 /* PostType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86DC39A2BBE7A0600926285 /* PostType.swift */; }; + F86DC39D2BC92FCD00926285 /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = F86DC39C2BC92FCD00926285 /* FirebaseStorage */; }; + F86E620A2BE35DFE00E26806 /* SearchRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86E62092BE35DFE00E26806 /* SearchRepository.swift */; }; F90DEA4F29327E4D002140E2 /* NavigationStackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8764C0D6291F85DF00E1593B /* NavigationStackModel.swift */; }; F90DEA5029327E5D002140E2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 87E99C7128F94EA8009B691F /* Assets.xcassets */; }; F90DEA5129327E62002140E2 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3D41EE07290A4C18008BE986 /* Launch Screen.storyboard */; }; @@ -329,6 +337,13 @@ A3FF01872918581E00384211 /* LicenseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseView.swift; sourceTree = ""; }; A3FF01892918F8EF00384211 /* apache.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = apache.txt; sourceTree = ""; }; A3FF018D291ACFA500384211 /* WithdrawalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawalView.swift; sourceTree = ""; }; + F86DC38F2BBC74CB00926285 /* CommunityRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityRepository.swift; sourceTree = ""; }; + F86DC3912BBDB7E800926285 /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; + F86DC3932BBDB7F200926285 /* Answer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Answer.swift; sourceTree = ""; }; + F86DC3952BBDBECF00926285 /* CommunityComment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityComment.swift; sourceTree = ""; }; + F86DC3972BBDE17C00926285 /* CommunityRepositoryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityRepositoryTest.swift; sourceTree = ""; }; + F86DC39A2BBE7A0600926285 /* PostType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostType.swift; sourceTree = ""; }; + F86E62092BE35DFE00E26806 /* SearchRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRepository.swift; sourceTree = ""; }; F9131B6A2922D38D00868A0E /* Keyword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keyword.swift; sourceTree = ""; }; F9136EB5293612310034AAB2 /* ShortcutsZipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsZipView.swift; sourceTree = ""; }; F91A72C0299915C500CA135A /* MoreCaptionTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreCaptionTextView.swift; sourceTree = ""; }; @@ -372,6 +387,7 @@ F94B435D2907B19A00987819 /* FirebaseAnalytics in Frameworks */, F94B43632907B19A00987819 /* FirebaseFirestoreCombine-Community in Frameworks */, F94B435F2907B19A00987819 /* FirebaseAuth in Frameworks */, + F86DC39D2BC92FCD00926285 /* FirebaseStorage in Frameworks */, F94B43612907B19A00987819 /* FirebaseFirestore in Frameworks */, 4D6A9EFF29A36E9C00D02522 /* WrappingHStack in Frameworks */, ); @@ -499,6 +515,7 @@ 87E99C6C28F94EA6009B691F /* HappyAnding */ = { isa = PBXGroup; children = ( + F86DC38E2BBC74B600926285 /* Repository */, 87E606AD2910623C00C3DA13 /* HappyAnding.entitlements */, 3D41EE06290A458B008BE986 /* Info.plist */, 87E99CC22901454D009B691F /* Extensions */, @@ -526,6 +543,7 @@ isa = PBXGroup; children = ( 87E99C7E28F94EA8009B691F /* HappyAndingTests.swift */, + F86DC3972BBDE17C00926285 /* CommunityRepositoryTest.swift */, ); path = HappyAndingTests; sourceTree = ""; @@ -690,6 +708,10 @@ F91F09DC29AE012600E04FA0 /* ShortcutGrade.swift */, 872B5D3C2A2E0FF9008DCC57 /* CurationType.swift */, A323D3C92AEE870700DDA716 /* SuggestionForm.swift */, + F86DC3912BBDB7E800926285 /* Post.swift */, + F86DC3932BBDB7F200926285 /* Answer.swift */, + F86DC3952BBDBECF00926285 /* CommunityComment.swift */, + F86DC39A2BBE7A0600926285 /* PostType.swift */, ); path = Model; sourceTree = ""; @@ -801,6 +823,15 @@ path = SettingViews; sourceTree = ""; }; + F86DC38E2BBC74B600926285 /* Repository */ = { + isa = PBXGroup; + children = ( + F86DC38F2BBC74CB00926285 /* CommunityRepository.swift */, + F86E62092BE35DFE00E26806 /* SearchRepository.swift */, + ); + path = Repository; + sourceTree = ""; + }; F9DB8ECB293B30EC00516CE1 /* Recovered References */ = { isa = PBXGroup; children = ( @@ -833,6 +864,7 @@ F94B43622907B19A00987819 /* FirebaseFirestoreCombine-Community */, 4D6A9EFE29A36E9C00D02522 /* WrappingHStack */, 4D93D0762A73C9330042CBA8 /* FirebaseMessaging */, + F86DC39C2BC92FCD00926285 /* FirebaseStorage */, ); productName = HappyAnding; productReference = 87E99C6A28F94EA6009B691F /* HappyAnding.app */; @@ -1040,6 +1072,7 @@ F96D45B72980301F000C2441 /* SubtitleTextView.swift in Sources */, A323D3CA2AEE870700DDA716 /* SuggestionForm.swift in Sources */, 4DF15D752A4ECE1F0014F854 /* ListCategoryShortcutViewModel.swift in Sources */, + F86DC3902BBC74CB00926285 /* CommunityRepository.swift in Sources */, 87E99CDB29042CCA009B691F /* Category.swift in Sources */, F9E7073A2BC6933000319533 /* SearchViewModel.swift in Sources */, 876B4F6F299E3D91009672D9 /* NavigationRouter.swift in Sources */, @@ -1049,7 +1082,9 @@ F9CAEF832914855900224B0A /* Date+String.swift in Sources */, 87E606B629114F7D00C3DA13 /* WriteNicknameView.swift in Sources */, 4D7D16072986BBD7008B3332 /* TextLiteral.swift in Sources */, + F86DC3922BBDB7E800926285 /* Post.swift in Sources */, 87E99CC128FFF2B5009B691F /* CategoryModalView.swift in Sources */, + F86DC3962BBDBECF00926285 /* CommunityComment.swift in Sources */, 87E606B829114FB200C3DA13 /* UserAuth.swift in Sources */, 8788E1A02A48408F007C3852 /* ExploreCurationViewModel.swift in Sources */, 8786B33E29ABA5A9000B46A1 /* View+Shape.swift in Sources */, @@ -1074,6 +1109,7 @@ 87E99CAD28FFF261009B691F /* ReadShortcutView.swift in Sources */, F930E0002BBD51EC003C2686 /* Seal.swift in Sources */, 4D5889E82AA36A52000C4849 /* AppDelegate.swift in Sources */, + F86DC39B2BBE7A0600926285 /* PostType.swift in Sources */, A33F74AE2908D8C800B8D0D0 /* CheckBoxShortcutCell.swift in Sources */, 87E606B22910649B00C3DA13 /* SignInWithAppleView.swift in Sources */, A38F3B1F2AE62E8D0036FCAC /* SuggestionFormView.swift in Sources */, @@ -1094,8 +1130,10 @@ 87E99CC9290145B8009B691F /* ListShortcutView.swift in Sources */, F92766552A61A032009C4EC2 /* WriteShortcutModalViewModel.swift in Sources */, A3FF01862918552E00384211 /* AboutTeamView.swift in Sources */, + F86E620A2BE35DFE00E26806 /* SearchRepository.swift in Sources */, 87E99C6E28F94EA6009B691F /* HappyAndingApp.swift in Sources */, 87E99CD32901465F009B691F /* ValidationCheckTextField.swift in Sources */, + F86DC3942BBDB7F200926285 /* Answer.swift in Sources */, F98017182BBC29A7004F2EA7 /* SCZ+Color.swift in Sources */, 87DBFB062A2127C0000CC442 /* CheckVersionView.swift in Sources */, A3FF01882918581E00384211 /* LicenseView.swift in Sources */, @@ -1122,6 +1160,7 @@ buildActionMask = 2147483647; files = ( 87E99C7F28F94EA8009B691F /* HappyAndingTests.swift in Sources */, + F86DC3982BBDE17C00926285 /* CommunityRepositoryTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1318,12 +1357,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = HappyAnding/HappyAnding.entitlements; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"HappyAnding/Preview Content\""; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HN3RL67C46; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = HappyAnding/Info.plist; @@ -1361,12 +1399,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = HappyAnding/HappyAnding.entitlements; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"HappyAnding/Preview Content\""; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HN3RL67C46; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = HappyAnding/Info.plist; @@ -1485,11 +1522,10 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HN3RL67C46; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -1503,7 +1539,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.happyanding.HappyAnding.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ShortcutsZipShareExtProfile_Dev_20240317; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -1517,11 +1552,10 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HN3RL67C46; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -1535,7 +1569,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.happyanding.HappyAnding.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ShortcutsZipShareExtProfile_Dev_20240317; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -1648,6 +1681,11 @@ package = F94B435B2907B19A00987819 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = "FirebaseFirestoreCombine-Community"; }; + F86DC39C2BC92FCD00926285 /* FirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + package = F94B435B2907B19A00987819 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseStorage; + }; F94B435C2907B19A00987819 /* FirebaseAnalytics */ = { isa = XCSwiftPackageProductDependency; package = F94B435B2907B19A00987819 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/HappyAnding/HappyAnding/Model/Answer.swift b/HappyAnding/HappyAnding/Model/Answer.swift new file mode 100644 index 00000000..077f3cea --- /dev/null +++ b/HappyAnding/HappyAnding/Model/Answer.swift @@ -0,0 +1,44 @@ +// +// Answer.swift +// HappyAnding +// +// Created by 임정욱 on 4/4/24. +// + +import Foundation + +struct Answer: Identifiable, Codable, Equatable, Hashable { + + let id: String + let postId : String + let createdAt: String + let author: String + + var content: String + var isAccepted: Bool + var images: [String] + var thumbnailImages: [String] + var likedBy: [String:Bool] + var likeCount: Int + + init(content: String, author: String, postId:String) { + + self.id = UUID().uuidString + self.createdAt = Date().getDate() + + self.content = content + self.isAccepted = false + self.author = author + self.postId = postId + self.images = [] + self.thumbnailImages = [] + + self.likeCount = 0 + self.likedBy = [:] + } + + var dictionary: [String: Any] { + let data = (try? JSONEncoder().encode(self)) ?? Data() + return (try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]) ?? [:] + } +} diff --git a/HappyAnding/HappyAnding/Model/CommunityComment.swift b/HappyAnding/HappyAnding/Model/CommunityComment.swift new file mode 100644 index 00000000..9ce515c0 --- /dev/null +++ b/HappyAnding/HappyAnding/Model/CommunityComment.swift @@ -0,0 +1,44 @@ +// +// CommunityComment.swift +// HappyAnding +// +// Created by 임정욱 on 4/4/24. +// + +import Foundation + + +struct CommunityComment: Identifiable, Codable, Equatable, Hashable { + + let id: String + let createdAt: String + let postId: String + let author: String + let parent: String? + + var content: String + var likeCount: Int + var likedBy: [String:Bool] + var isAccepted: Bool + + init(content: String, author: String,postId : String, parent: String? = nil) { + + self.id = UUID().uuidString + self.createdAt = Date().getDate() + + self.content = content + self.author = author + self.parent = parent + self.postId = postId + + self.likeCount = 0 + self.likedBy = [:] + self.isAccepted = false + } + + var dictionary: [String: Any] { + let data = (try? JSONEncoder().encode(self)) ?? Data() + return (try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) ?? [:] + } + +} diff --git a/HappyAnding/HappyAnding/Model/Post.swift b/HappyAnding/HappyAnding/Model/Post.swift new file mode 100644 index 00000000..2406b098 --- /dev/null +++ b/HappyAnding/HappyAnding/Model/Post.swift @@ -0,0 +1,47 @@ +// +// Post.swift +// HappyAnding +// +// Created by 임정욱 on 4/4/24. +// + +import Foundation + +struct Post: Identifiable, Codable, Equatable, Hashable { + + let id : String + let type: PostType + let createdAt: String + let author: String + + var content: String + var shortcuts: [String] + var images: [String] + var thumbnailImages: [String] + var likedBy: [String:Bool] + var likeCount: Int + var commentCount: Int + + init(type: PostType, content: String, author: String, shortcuts: [String] = []) { + + self.id = UUID().uuidString + self.createdAt = Date().getDate() + + self.type = type + self.content = content + self.author = author + self.shortcuts = shortcuts + self.images = [] + self.thumbnailImages = [] + + self.likeCount = 0 + self.commentCount = 0 + self.likedBy = [:] + + } + + var dictionary: [String: Any] { + let data = (try? JSONEncoder().encode(self)) ?? Data() + return (try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]) ?? [:] + } +} diff --git a/HappyAnding/HappyAnding/Model/PostType.swift b/HappyAnding/HappyAnding/Model/PostType.swift new file mode 100644 index 00000000..64796426 --- /dev/null +++ b/HappyAnding/HappyAnding/Model/PostType.swift @@ -0,0 +1,13 @@ +// +// PostType.swift +// HappyAnding +// +// Created by 임정욱 on 4/4/24. +// + +import Foundation + +enum PostType: String, Codable { + case General = "General" + case Question = "Question" +} diff --git a/HappyAnding/HappyAnding/Repository/CommunityRepository.swift b/HappyAnding/HappyAnding/Repository/CommunityRepository.swift new file mode 100644 index 00000000..bbeb176d --- /dev/null +++ b/HappyAnding/HappyAnding/Repository/CommunityRepository.swift @@ -0,0 +1,840 @@ +// +// File.swift +// HappyAnding +// +// Created by 임정욱 on 4/3/24. +// + +import Foundation +import FirebaseCore +import FirebaseFirestore +import FirebaseStorage +import FirebaseFirestoreSwift +import FirebaseAuth +import SwiftUI + + +class CommunityRepository { + + private let db = Firestore.firestore() + private let storage = Storage.storage().reference() + + private let postCollection: String = "Post" + private let communityCommentCollection: String = "CommunityComment" + private let answerCollection: String = "Answer" + + + private func uploadImages(images: [UIImage], completion: @escaping ([String]?) -> Void) { + var imageURLs = [String]() + let imageUploadGroup = DispatchGroup() + + for image in images { + imageUploadGroup.enter() + let imageData = image.pngData()! + let imageId = UUID().uuidString + let imageRef = storage.child("community/\(imageId).png") + + let metadata = StorageMetadata() + metadata.contentType = "image/png" + + imageRef.putData(imageData, metadata: metadata) { metadata, error in + if let error = error { + print(error.localizedDescription) + imageUploadGroup.leave() + return + } + + imageRef.downloadURL { (url, error) in + if let error = error { + print(error.localizedDescription) + imageUploadGroup.leave() + return + } + + if let downloadURL = url { + imageURLs.append(downloadURL.absoluteString) + imageUploadGroup.leave() + } + } + } + } + + imageUploadGroup.notify(queue: .main) { + if imageURLs.isEmpty { + completion(nil) + } else { + completion(imageURLs) + } + } + } + + + private func deleteImages(with urls: [String], completion: @escaping (Bool) -> Void) { + let storage = Storage.storage() + let dispatchGroup = DispatchGroup() + + var deleteErrors = false + + for url in urls { + let ref = storage.reference(forURL: url) + + dispatchGroup.enter() + + ref.delete { error in + if let error = error { + deleteErrors = true + } + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: .main) { + completion(!deleteErrors) + } + } + + + + + + +//MARK: - 글 관련 함수 + + + // 모든 글 가져오기 + func getAllPosts(completion: @escaping ([Post]) -> Void) { + db.collection(postCollection) + .order(by: "createdAt", descending: true) // 최신 글부터 정렬 + .getDocuments { snapshot, error in + if error != nil { + completion([]) + return + } + let posts = snapshot?.documents.compactMap { document in + try? document.data(as: Post.self) + } ?? [] + completion(posts) + } + } + + // 글 가져오기 무한스크롤 + func getPosts(limit: Int, lastCreatedAt: String? = nil, completion: @escaping ([Post]) -> Void) { + var query: Query = db.collection(postCollection) + .order(by: "createdAt", descending: true) + .limit(to: limit) + + if let lastCreatedAt = lastCreatedAt { + query = query.start(after: [lastCreatedAt]) + } + + query.getDocuments { snapshot, error in + guard let snapshot = snapshot, error == nil else { + completion([]) + return + } + + let posts = snapshot.documents.compactMap { document -> Post? in + try? document.data(as: Post.self) + } + + completion(posts) + } + } + + + // 글 생성 + func createPost(post: Post, images: [UIImage]? = nil, thumbnailImages: [UIImage]? = nil, completion: @escaping (Bool) -> Void) { + let documentId = post.id + + do { + try db.collection(postCollection).document(documentId).setData(from: post) { error in + if let error = error { + print(error.localizedDescription) + completion(false) + return + } + + var imageURLs: [String] = [] + var thumbnailURLs: [String] = [] + let dispatchGroup = DispatchGroup() + + + // 일반 이미지 업로드 + if let images = images, !images.isEmpty { + dispatchGroup.enter() + self.uploadImages(images: images) { urls in + if let urls = urls { + imageURLs = urls + } + dispatchGroup.leave() + } + } + + // 썸네일 이미지 업로드 + if let thumbnails = thumbnailImages, !thumbnails.isEmpty { + dispatchGroup.enter() + self.uploadImages(images: thumbnails) { urls in + if let urls = urls { + thumbnailURLs = urls + } + dispatchGroup.leave() + } + } + + // 모든 이미지 업로드가 완료된 후 Firestore 문서 업데이트 + dispatchGroup.notify(queue: .main) { + var updateData = [String: Any]() + if !imageURLs.isEmpty { + updateData["images"] = imageURLs + } + if !thumbnailURLs.isEmpty { + updateData["thumbnailImages"] = thumbnailURLs + } + + if !updateData.isEmpty { + self.db.collection(self.postCollection).document(documentId).updateData(updateData) { error in + if let error = error { + print(error.localizedDescription) + completion(false) + } else { + completion(true) + } + } + } else { + completion(true) + } + } + } + } catch { + completion(false) + } + } + + // 글 업데이트 + func updatePost(postId: String, content: String? = nil, shortcuts: [String]? = nil, images: [UIImage]? = nil, thumbnailImages: [UIImage]? = nil, completion: @escaping (Bool) -> Void) { + + let documentRef = db.collection(postCollection).document(postId) + + var updateFields: [String: Any] = [:] + if let content = content { + updateFields["content"] = content + } + if let shortcuts = shortcuts { + updateFields["shortcuts"] = shortcuts + } + + documentRef.updateData(updateFields) { error in + if let error = error { + print(error.localizedDescription) + completion(false) + return + } + + + guard images != nil || thumbnailImages != nil else { + completion(true) + return + } + + // 이미지 삭제 및 업로드 + documentRef.getDocument { document, error in + if let document = document, document.exists { + let oldImageUrls = document.data()?["images"] as? [String] ?? [] + let oldThumbnailUrls = document.data()?["thumbnailImages"] as? [String] ?? [] + + self.deleteImages(with: oldImageUrls + oldThumbnailUrls) { success in + if !success { + completion(false) + return + } + + // 새 이미지와 썸네일 이미지 업로드 + self.uploadImages(images: images ?? []) { newImageUrls in + self.uploadImages(images: thumbnailImages ?? []) { newThumbnailUrls in + var imageUpdateFields: [String: Any] = [:] + if let newImageUrls = newImageUrls { + imageUpdateFields["images"] = newImageUrls + } + if let newThumbnailUrls = newThumbnailUrls { + imageUpdateFields["thumbnailImages"] = newThumbnailUrls + } + + if !imageUpdateFields.isEmpty { + documentRef.updateData(imageUpdateFields) { error in + completion(error == nil) + } + } else { + completion(true) + } + } + } + } + } else { + completion(false) + } + } + } + } + + + + + // 글 삭제 + func deletePost(postId: String, completion: @escaping (Bool) -> Void) { + let group = DispatchGroup() + var overallSuccess = true + + let documentRef = db.collection(postCollection).document(postId) + + group.enter() + documentRef.getDocument { document, error in + if let document = document, document.exists { + let imageUrls = document.data()?["images"] as? [String] ?? [] + let thumbnailUrls = document.data()?["thumbnailImages"] as? [String] ?? [] + let allUrls = imageUrls + thumbnailUrls + if !allUrls.isEmpty { + self.deleteImages(with: allUrls) { success in + if !success { + overallSuccess = false + } + group.leave() + } + } else { + group.leave() + } + + } else { + overallSuccess = false + } + + documentRef.delete { error in + if error != nil { + overallSuccess = false + } + } + } + + + // 관련된 답변(Answer) 삭제 + group.enter() + db.collection(answerCollection).whereField("postId", isEqualTo: postId).getDocuments { (snapshot, error) in + guard let documents = snapshot?.documents else { + overallSuccess = false + group.leave() + return + } + for document in documents { + self.deleteAnswer(answerId:document.data()["id"] as? String ?? "") { success in + if (!success) { + overallSuccess = false + } + } + } + group.leave() + } + + // 관련된 댓글(CommunityComment) 삭제 + group.enter() + db.collection(communityCommentCollection).whereField("postId", isEqualTo: postId).getDocuments { (snapshot, error) in + guard let documents = snapshot?.documents else { + overallSuccess = false + group.leave() + return + } + for document in documents { + document.reference.delete() + } + group.leave() + } + + group.notify(queue: .main) { + completion(overallSuccess) + } + } + + + // 글에 좋아요 추가 + func likePost(postId: String, userId: String, completion: @escaping (Bool) -> Void) { + let postRef = db.collection(postCollection).document(postId) + + postRef.getDocument { (document, error) in + if let document = document, let data = document.data(), error == nil { + var likedBy = data["likedBy"] as? [String: Bool] ?? [:] + + if likedBy[userId] != true { + likedBy[userId] = true + let likeCount = (data["likeCount"] as? Int ?? 0) + 1 + + postRef.updateData([ + "likedBy": likedBy, + "likeCount": likeCount + ]) { error in + if error != nil { + completion(false) + } else { + completion(true) + } + } + } else { + completion(true) + } + } else { + completion(false) + } + } + } + + // 글에 좋아요 제거 + func unlikePost(postId: String, userId: String, completion: @escaping (Bool) -> Void) { + let postRef = db.collection(postCollection).document(postId) + + postRef.getDocument { (document, error) in + if let document = document, let data = document.data(), error == nil { + var likedBy = data["likedBy"] as? [String: Bool] ?? [:] + + if likedBy[userId] == true { + likedBy.removeValue(forKey: userId) + let likeCount = max((data["likeCount"] as? Int ?? 0) - 1, 0) + + postRef.updateData([ + "likedBy": likedBy, + "likeCount": likeCount + ]) { error in + if error != nil { + completion(false) + } else { + completion(true) + } + } + } else { + completion(true) + } + } else { + completion(false) + } + } + } + + +//MARK: - 답변 관련 함수 + + func getAnswers(for postId: String, completion: @escaping ([Answer]) -> Void) { + db.collection(answerCollection) + .whereField("postId", isEqualTo: postId) + .order(by: "createdAt", descending: true) + .getDocuments { snapshot, error in + + if error != nil { + completion([]) + return + } + + let answers = snapshot?.documents.compactMap { document -> Answer? in + try? document.data(as: Answer.self) + } ?? [] + completion(answers) + } + } + + // 답변 생성 + func createAnswer(answer: Answer, images: [UIImage]? = nil, thumbnailImages: [UIImage]? = nil, completion: @escaping (Bool) -> Void) { + let documentId = answer.id // Answer 객체의 ID를 사용 + + do { + try db.collection(answerCollection).document(documentId).setData(from: answer) { error in + if let error = error { + completion(false) + return + } + + var imageURLs: [String] = [] + var thumbnailURLs: [String] = [] + let dispatchGroup = DispatchGroup() + + // 일반 이미지 업로드 + if let images = images, !images.isEmpty { + dispatchGroup.enter() + self.uploadImages(images: images) { urls in + if let urls = urls { + imageURLs = urls + } + dispatchGroup.leave() + } + } + + // 썸네일 이미지 업로드 + if let thumbnails = thumbnailImages, !thumbnails.isEmpty { + dispatchGroup.enter() + self.uploadImages(images: thumbnails) { urls in + if let urls = urls { + thumbnailURLs = urls + } + dispatchGroup.leave() + } + } + + // 모든 이미지 업로드가 완료된 후 Firestore 문서 업데이트 + dispatchGroup.notify(queue: .main) { + var updateData = [String: Any]() + if !imageURLs.isEmpty { + updateData["images"] = imageURLs + } + if !thumbnailURLs.isEmpty { + updateData["thumbnailImages"] = thumbnailURLs + } + + if !updateData.isEmpty { + self.db.collection(self.answerCollection).document(documentId).updateData(updateData) { error in + completion(error == nil) + } + } else { + completion(true) + } + } + } + } catch { + completion(false) + } + } + + + // 답변 업데이트 + func updateAnswer(answerId: String, content: String? = nil, images: [UIImage]? = nil, thumbnailImages: [UIImage]? = nil, completion: @escaping (Bool) -> Void) { + + let documentRef = db.collection(answerCollection).document(answerId) + + var updateFields: [String: Any] = [:] + if let content = content { + updateFields["content"] = content + } + + documentRef.updateData(updateFields) { error in + if let error = error { + completion(false) + return + } + + // 이미지 업데이트 로직 + guard images != nil || thumbnailImages != nil else { + completion(true) + return + } + + // 이미지 삭제 및 업로드 + documentRef.getDocument { document, error in + if let document = document, document.exists { + let oldImageUrls = document.data()?["images"] as? [String] ?? [] + let oldThumbnailUrls = document.data()?["thumbnailImages"] as? [String] ?? [] + + self.deleteImages(with: oldImageUrls + oldThumbnailUrls) { success in + if !success { + completion(false) + return + } + + // 새 이미지와 썸네일 이미지 업로드 + self.uploadImages(images: images ?? []) { newImageUrls in + self.uploadImages(images: thumbnailImages ?? []) { newThumbnailUrls in + var imageUpdateFields: [String: Any] = [:] + if let newImageUrls = newImageUrls { + imageUpdateFields["images"] = newImageUrls + } + if let newThumbnailUrls = newThumbnailUrls { + imageUpdateFields["thumbnailImages"] = newThumbnailUrls + } + + if !imageUpdateFields.isEmpty { + documentRef.updateData(imageUpdateFields) { error in + completion(error == nil) + } + } else { + completion(true) + } + } + } + } + } else { + completion(false) + } + } + } + } + + // 답변 채택 + func acceptAnswer(answerId: String, completion: @escaping (Bool) -> Void) { + let answerRef = db.collection(answerCollection).document(answerId) + + answerRef.updateData(["isAccepted": true]) { error in + if error != nil { + completion(false) + } else { + completion(true) + } + } + } + + // 답변 삭제 + func deleteAnswer(answerId: String, completion: @escaping (Bool) -> Void) { + let db = Firestore.firestore() + let group = DispatchGroup() + var overallSuccess = true + + group.enter() + db.collection(answerCollection).document(answerId).getDocument { document, error in + if let document = document, document.exists { + let imageUrls = document.data()?["images"] as? [String] ?? [] + let thumbnailUrls = document.data()?["thumbnailImages"] as? [String] ?? [] + let allUrls = imageUrls + thumbnailUrls + + if !allUrls.isEmpty { + self.deleteImages(with: allUrls) { success in + if !success { + overallSuccess = false + } + group.leave() + } + } else { + group.leave() + } + } else { + overallSuccess = false + group.leave() + } + + db.collection(self.answerCollection).document(answerId).delete { error in + if error != nil { + overallSuccess = false + } + } + } + + group.notify(queue: .main) { + completion(overallSuccess) + } + } + + // 답변 좋아요 + func likeAnswer(answerId: String, userId: String, completion: @escaping (Bool) -> Void) { + let answerRef = db.collection(answerCollection).document(answerId) + + answerRef.getDocument { (document, error) in + if let document = document, let data = document.data(), error == nil { + var likedBy = data["likedBy"] as? [String: Bool] ?? [:] + + if likedBy[userId] != true { + likedBy[userId] = true + + let likeCount = (data["likeCount"] as? Int ?? 0) + 1 + + answerRef.updateData([ + "likedBy": likedBy, + "likeCount": likeCount + ]) { error in + if error != nil { + completion(false) + } else { + completion(true) + } + } + } else { + completion(true) + } + } else { + completion(false) + } + } + } + + + // 답변 좋아요 취소 + func unlikeAnswer(answerId: String, userId: String, completion: @escaping (Bool) -> Void) { + let answerRef = db.collection(answerCollection).document(answerId) + + answerRef.getDocument { documentSnapshot, error in + guard let document = documentSnapshot, let data = document.data(), error == nil else { + completion(false) + return + } + + var likedBy = data["likedBy"] as? [String: Bool] ?? [:] + + if likedBy[userId] == true { + likedBy.removeValue(forKey: userId) + + let likeCount = max((data["likeCount"] as? Int ?? 0) - 1, 0) + + answerRef.updateData([ + "likedBy": likedBy, + "likeCount": likeCount + ]) { error in + if error != nil { + completion(false) + } else { + completion(true) + } + } + } else { + completion(true) + } + } + } + +//MARK: - 댓글 관련 함수 + + // 특정 게시물의 댓글 가져오기 + func getComments(postId: String, completion: @escaping ([CommunityComment]) -> Void) { + db.collection(communityCommentCollection) + .whereField("postId", isEqualTo: postId) + .order(by: "createdAt", descending: true) + .getDocuments { (snapshot, error) in + guard let documents = snapshot?.documents, error == nil else { + completion([]) + return + } + let comments = documents.compactMap { document -> CommunityComment? in + try? document.data(as: CommunityComment.self) + } + completion(comments) + } + } + + // 댓글 생성 + func createComment(comment: CommunityComment, completion: @escaping (Bool) -> Void) { + let documentId = comment.id + let postId = comment.postId + + + do { + try db.collection(communityCommentCollection).document(documentId).setData(from: comment) { error in + if error != nil { + completion(false) + } else { + let postRef = self.db.collection(self.postCollection).document(postId) + postRef.updateData(["commentCount": FieldValue.increment(1.0)]) { error in + if error != nil { + completion(false) + } else { + + completion(true) + } + } + } + } + } catch { + completion(false) + } + } + + // 댓글 수정 + func updateComment(commentId: String, content: String?, completion: @escaping (Bool) -> Void) { + var updates: [String: Any] = [:] + + if let newContent = content { + updates["content"] = newContent + } + + if !updates.isEmpty { + db.collection(communityCommentCollection).document(commentId).updateData(updates) { error in + if let error = error { + completion(false) + } else { + completion(true) + } + } + } else { + completion(true) + } + } + + // 댓글 삭제 + func deleteComment(commentId: String, completion: @escaping (Bool) -> Void) { + let commentRef = db.collection(communityCommentCollection).document(commentId) + + commentRef.getDocument { (document, error) in + guard let document = document, let commentData = document.data(), let postId = commentData["postId"] as? String else { + completion(false) + return + } + + commentRef.delete() { error in + if error != nil { + completion(false) + } else { + let postRef = self.db.collection(self.postCollection).document(postId) + postRef.updateData(["commentCount": FieldValue.increment(-1.0)]) { error in + if error != nil { + completion(false) + } else { + completion(true) + } + } + } + } + } + } + + // 댓글에 좋아요 추가 + func likeComment(commentId: String, userId: String, completion: @escaping (Bool) -> Void) { + let commentRef = db.collection(communityCommentCollection).document(commentId) + + commentRef.getDocument { (document, error) in + if let document = document, let data = document.data(), error == nil { + var likedBy = data["likedBy"] as? [String: Bool] ?? [:] + + if likedBy[userId] != true { + likedBy[userId] = true + + let likeCount = (data["likeCount"] as? Int ?? 0) + 1 + + commentRef.updateData([ + "likedBy": likedBy, + "likeCount": likeCount + ]) { error in + if error != nil { + completion(false) + } else { + completion(true) + } + } + } else { + completion(true) + } + } else { + completion(false) + } + } + } + + // 댓글에 좋아요 제거 + func unlikeComment(commentId: String, userId: String, completion: @escaping (Bool) -> Void) { + let commentRef = db.collection(communityCommentCollection).document(commentId) + + commentRef.getDocument { documentSnapshot, error in + guard let document = documentSnapshot, let data = document.data(), error == nil else { + completion(false) + return + } + + var likedBy = data["likedBy"] as? [String: Bool] ?? [:] + + if likedBy[userId] == true { + likedBy.removeValue(forKey: userId) + + let likeCount = max((data["likeCount"] as? Int ?? 0) - 1, 0) + + commentRef.updateData([ + "likedBy": likedBy, + "likeCount": likeCount + ]) { error in + if error != nil { + completion(false) + } else { + completion(true) + } + } + } else { + completion(true) + } + } + } +} diff --git a/HappyAnding/HappyAnding/Repository/SearchRepository.swift b/HappyAnding/HappyAnding/Repository/SearchRepository.swift new file mode 100644 index 00000000..4432c57b --- /dev/null +++ b/HappyAnding/HappyAnding/Repository/SearchRepository.swift @@ -0,0 +1,74 @@ +//// +//// SearchRepository.swift +//// HappyAnding +//// +//// Created by 임정욱 on 5/2/24. +//// +// +//import Foundation +//import FirebaseCore +//import FirebaseFirestore +// +// +//class SearchRepository { +// +// +// private let db = Firestore.firestore() +// private let postCollection: String = "Post" +// private let shortcutCollection: String = "Shortcut" +// +// +// +// public func searchContentAndShortcuts(keyword: String, completion: @escaping ([[Any]]) -> Void) { +// let db = Firestore.firestore() +// let postsRef = db.collection(postCollection) +// let shortcutsRef = db.collection(shortcutCollection) +// +// let group = DispatchGroup() +// var postsResults = [Post]() +// var shortcutsResults = [Shortcuts]() +// var errors: [Error] = [] +// +// let searchFieldsShortcuts = ["title", "subtitle", "description"] +// for field in searchFieldsShortcuts { +// group.enter() +// shortcutsRef.whereField(field, arrayContains: keyword).getDocuments { (snapshot, error) in +// defer { group.leave() } +// if let snapshot = snapshot { +// shortcutsResults += snapshot.documents.compactMap { document -> Shortcuts? in +// try? document.data(as: Shortcuts.self) +// } +// } else if let error = error { +// errors.append(error) +// } +// } +// } +// +// let searchFieldsPosts = ["title", "content"] +// for field in searchFieldsPosts { +// group.enter() +// postsRef.whereField(field, arrayContains: keyword).getDocuments { (snapshot, error) in +// defer { group.leave() } +// if let snapshot = snapshot { +// postsResults += snapshot.documents.compactMap { document -> Post? in +// try? document.data(as: Post.self) +// } +// } else if let error = error { +// errors.append(error) +// } +// } +// } +// +// group.notify(queue: .main) { +// if errors.isEmpty { +// let combinedResults = [shortcutsResults as [Any], postsResults as [Any]] +// completion(combinedResults) +// } else { +// completion([]) +// } +// } +// } +// +// +// +//} diff --git a/HappyAnding/HappyAnding/Views/SearchViewModel.swift b/HappyAnding/HappyAnding/Views/SearchViewModel.swift index 819ca8f3..9e1dfce7 100644 --- a/HappyAnding/HappyAnding/Views/SearchViewModel.swift +++ b/HappyAnding/HappyAnding/Views/SearchViewModel.swift @@ -8,62 +8,62 @@ import SwiftUI //TODO: 레포지터리 머지 시 삭제 필요 -enum PostType: String, Codable { - case General = "General" - case Question = "Question" -} - -struct Post: Identifiable, Codable, Equatable, Hashable { - - let id : String - let type: PostType - let createdAt: String - let author: String - - var content: String - var shortcuts: [String] - var images: [String] - var likedBy: [String:Bool] - var likeCount: Int - var commentCount: Int - - init(type: PostType, content: String, author: String, shortcuts: [String] = [], images: [String] = []) { - - self.id = UUID().uuidString - self.createdAt = Date().getDate() - - self.type = type - self.content = content - self.author = author - self.shortcuts = shortcuts - self.images = images - - self.likeCount = 0 - self.commentCount = 0 - self.likedBy = [:] - - } - - init() { - self.id = UUID().uuidString - self.createdAt = Date().getDate() - - self.type = .General - self.content = "가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하" - self.author = "author" - self.shortcuts = ["shortcuts"] - self.images = ["images"] - - self.likeCount = 0 - self.commentCount = 0 - self.likedBy = [:] - } - - var dictionary: [String: Any] { - let data = (try? JSONEncoder().encode(self)) ?? Data() - return (try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]) ?? [:] - } -} +//enum PostType: String, Codable { +// case General = "General" +// case Question = "Question" +//} +// +//struct Post: Identifiable, Codable, Equatable, Hashable { +// +// let id : String +// let type: PostType +// let createdAt: String +// let author: String +// +// var content: String +// var shortcuts: [String] +// var images: [String] +// var likedBy: [String:Bool] +// var likeCount: Int +// var commentCount: Int +// +// init(type: PostType, content: String, author: String, shortcuts: [String] = [], images: [String] = []) { +// +// self.id = UUID().uuidString +// self.createdAt = Date().getDate() +// +// self.type = type +// self.content = content +// self.author = author +// self.shortcuts = shortcuts +// self.images = images +// +// self.likeCount = 0 +// self.commentCount = 0 +// self.likedBy = [:] +// +// } +// +// init() { +// self.id = UUID().uuidString +// self.createdAt = Date().getDate() +// +// self.type = .General +// self.content = "가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하" +// self.author = "author" +// self.shortcuts = ["shortcuts"] +// self.images = ["images"] +// +// self.likeCount = 0 +// self.commentCount = 0 +// self.likedBy = [:] +// } +// +// var dictionary: [String: Any] { +// let data = (try? JSONEncoder().encode(self)) ?? Data() +// return (try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]) ?? [:] +// } +//} final class SearchViewModel: ObservableObject { private let shortcutsZipViewModel = ShortcutsZipViewModel.share diff --git a/HappyAnding/HappyAndingTests/CommunityRepositoryTest.swift b/HappyAnding/HappyAndingTests/CommunityRepositoryTest.swift new file mode 100644 index 00000000..26789236 --- /dev/null +++ b/HappyAnding/HappyAndingTests/CommunityRepositoryTest.swift @@ -0,0 +1,394 @@ +// +// CommunityRepositoryTest.swift +// HappyAndingTests +// +// Created by 임정욱 on 4/4/24. +// + +import XCTest +@testable import HappyAnding // YourAppName을 앱의 이름으로 변경 + +class FirestoreTests: XCTestCase { + + + var repository: CommunityRepository! + + override func setUp() { + super.setUp() + repository = CommunityRepository() + } + + override func tearDown() { + repository = nil + super.tearDown() + } + + private let testPostId = "8F242D05-3FB4-4604-880A-93A99B3F77AF" + private let testAnswerId = "CECD1EBA-2157-41ED-AA24-AEBD4D105272" + private let testCommentId = "C644F4BF-5F32-4F67-B2D8-18F182A1FB2C" + + +//MARK: - 글 관련 테스트 + + + + // 모든 글 가져오기 테스트 + func testGetAllPosts() { + let expectation = self.expectation(description: "getPosts") + + repository.getAllPosts { posts in + + print("\n") + + for post in posts { + print(post) + } + + print("\n") + + XCTAssertNotNil(posts, "Should not return nil") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 무한스크롤 글 가져오기 함수 테스트 + func testGetPosts() { + let expectation = self.expectation(description: "getPosts") + let limit = 10 + let lastCreatedAt: String? = "20240411102228" + + repository.getPosts(limit: limit, lastCreatedAt: lastCreatedAt) { posts in + + print("\n") + + for post in posts { + print(post) + } + + print("\n") + + XCTAssertNotNil(posts, "Should not return nil") + + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + + // 글 생성 테스트 + func testCreatePost() { + let expectation = self.expectation(description: "createPost") + + let newPost = Post(type: PostType.General, content: "테스트용", author:"1") + + repository.createPost(post: newPost) { success in + + XCTAssertTrue(success, "Post Create should succed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 글 생성 테스트 with Images + func testCreatePostWithImages() { + let expectation = self.expectation(description: "Completion handler invoked") + + let testPost = Post(type:PostType.General, content: "This is a test post", author:"1") + let testImages = [UIImage(named: "updateAppIcon")!, UIImage(named: "updateAppIcon")!] + let testthumbnailImages = [UIImage(named: "updateAppIcon")!, UIImage(named: "updateAppIcon")!] + + repository.createPost(post: testPost, images: testImages, thumbnailImages: testthumbnailImages) { success in + + XCTAssertTrue(success, "Post Create should succed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 글 업데이트 테스트 + func testUpdatePost() { + let expectation = self.expectation(description: "updatePost") + + let testImages = [UIImage(named: "updateAppIcon")!, UIImage(named: "updateAppIcon")!] + let testthumbnailImages = [UIImage(named: "updateAppIcon")!, UIImage(named: "updateAppIcon")!] + + let postId = testPostId + repository.updatePost(postId: postId, content: "Updated Content", shortcuts: ["Shortcut1"], images:testImages, thumbnailImages: testthumbnailImages) { success in + XCTAssertTrue(success, "Post update should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 글 삭제 테스트 + func testDeletePost() { + let expectation = self.expectation(description: "deletePost") + + let postId = testPostId + repository.deletePost(postId: postId) { success in + XCTAssertTrue(success, "Post deletion should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 20, handler: nil) + } + + + // 글에 좋아요 추가 테스트 + func testLikePost() { + let expectation = self.expectation(description: "likePost") + + let postId = testPostId + let userId = "2" + + repository.likePost(postId: postId, userId: userId) { success in + XCTAssertTrue(success, "Liking a post should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 글에 좋아요 제거 테스트 + func testUnlikePost() { + let expectation = self.expectation(description: "unlikePost") + + let postId = testPostId + let userId = "2" + + repository.unlikePost(postId: postId, userId: userId) { success in + XCTAssertTrue(success, "Unliking a post should succeed.") + expectation.fulfill() + } + + + waitForExpectations(timeout: 5, handler: nil) + } + +//MARK: - 답변 관련 테스트 + + // 특정 게시물의 모든 답변을 가져오는 함수 테스트 + func testGetAnswers() { + let expectation = self.expectation(description: "getAnswers") + let testPostId = testPostId + + repository.getAnswers(for: testPostId) { answers in + + print("\n") + + for answer in answers { + print(answer) + } + + print("\n") + + XCTAssertNotNil(answers, "Should not return nil") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 새로운 답변을 생성하는 함수 테스트 + func testCreateAnswer() { + let expectation = self.expectation(description: "createAnswer") + let answer = Answer(content: "Test answer", author: "1", postId: testPostId) + + repository.createAnswer(answer: answer) { success in + XCTAssertTrue(success, "Answer creation should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 새로운 답변을 생성하는 함수 with Images 테스트 + func testCreateAnswerWithImages() { + let expectation = self.expectation(description: "createAnswer") + let answer = Answer(content: "Test answer", author: "1", postId: testPostId) + + let testImages = [UIImage(named: "updateAppIcon")!, UIImage(named: "updateAppIcon")!] + let testthumbnailImages = [UIImage(named: "updateAppIcon")!, UIImage(named: "updateAppIcon")!] + + repository.createAnswer(answer: answer, images: testImages, thumbnailImages: testthumbnailImages) { success in + XCTAssertTrue(success, "Answer creation should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + + + // 기존의 답변을 업데이트하는 함수 테스트 + func testUpdateAnswer() { + let expectation = self.expectation(description: "updateAnswer") + let answerId = testAnswerId + + let testImages = [UIImage(named: "updateAppIcon")!, UIImage(named: "updateAppIcon")!] + let testthumbnailImages = [UIImage(named: "updateAppIcon")!, UIImage(named: "updateAppIcon")!] + + repository.updateAnswer(answerId: answerId, content: "Updated content", images: testImages, thumbnailImages: testthumbnailImages) { success in + XCTAssertTrue(success, "Answer update should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + + // 답변을 채택하는 함수 테스트 + func testAcceptAnswer() { + let expectation = self.expectation(description: "acceptAnswer") + let answerId = testAnswerId + + repository.acceptAnswer(answerId: answerId) { success in + XCTAssertTrue(success, "Answer acception should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 특정 답변을 삭제하는 함수 테스트 + func testDeleteAnswer() { + let expectation = self.expectation(description: "deleteAnswer") + let answerId = testAnswerId + + repository.deleteAnswer(answerId: answerId) { success in + XCTAssertTrue(success, "Answer deletion should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 답변에 좋아요를 누르는 함수 테스트 + func testLikeAnswer() { + let expectation = self.expectation(description: "likeAnswer") + let answerId = testAnswerId + let userId = "1" + + repository.likeAnswer(answerId: answerId, userId: userId) { success in + XCTAssertTrue(success, "Liking an answer should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 답변에 좋아요를 제거하는 함수 테스트 + func testUnlikeAnswer() { + let expectation = self.expectation(description: "unlikeAnswer") + let answerId = testAnswerId + let userId = "1" + + repository.unlikeAnswer(answerId: answerId, userId: userId) { success in + XCTAssertTrue(success, "Unliking an answer should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + +//MARK: - 댓글 관련 테스트 + + // 특정 게시물의 모든 댓글을 가져오는 함수 테스트 + func testGetComments() { + let expectation = self.expectation(description: "getComments") + let testPostId = testPostId + + repository.getComments(postId: testPostId) { comments in + + print("\n") + + for comment in comments { + print(comment) + } + + print("\n") + + XCTAssertNotNil(comments, "Should not return nil") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 새로운 댓글을 생성하는 함수 테스트 + func testCreateComment() { + let expectation = self.expectation(description: "createComment") + let comment = CommunityComment(content: "Test comment", author: "1", postId: testPostId) + + repository.createComment(comment: comment) { success in + XCTAssertTrue(success, "Comment creation should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 기존의 댓글을 업데이트하는 함수 테스트 + func testUpdateComment() { + let expectation = self.expectation(description: "updateComment") + let commentId = testCommentId + let newContent = "Updated content" + + repository.updateComment(commentId: commentId, content: newContent) { success in + XCTAssertTrue(success, "Comment update should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 특정 댓글을 삭제하는 함수 테스트 + func testDeleteComment() { + let expectation = self.expectation(description: "deleteComment") + let commentId = testCommentId + + repository.deleteComment(commentId: commentId) { success in + XCTAssertTrue(success, "Comment deletion should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 댓글에 좋아요를 추가하는 함수 테스트 + func testLikeComment() { + let expectation = self.expectation(description: "likeComment") + let commentId = testCommentId + let userId = "1" + + repository.likeComment(commentId: commentId, userId: userId) { success in + XCTAssertTrue(success, "Liking a comment should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + // 댓글에 추가된 좋아요를 제거하는 함수 테스트 + func testUnlikeComment() { + let expectation = self.expectation(description: "unlikeComment") + let commentId = testCommentId + let userId = "1" + + repository.unlikeComment(commentId: commentId, userId: userId) { success in + XCTAssertTrue(success, "Unliking a comment should succeed.") + expectation.fulfill() + } + + waitForExpectations(timeout: 5, handler: nil) + } + + + +}