From 96e9da90c5051488791ec202738628a26aec10c5 Mon Sep 17 00:00:00 2001 From: HyunJaeyeon Date: Thu, 28 Nov 2024 17:46:41 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20#76-FirestoreService=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetchAnimeTitles: 처음에 애니메 타이틀만 불러와서 저장 - fetchAnimeDetailsAndStore: 특정 애니메의 정보들만 불러와 저장 --- PepperoniV2/Service/FirestoreService.swift | 172 +++++++++++---------- 1 file changed, 89 insertions(+), 83 deletions(-) diff --git a/PepperoniV2/Service/FirestoreService.swift b/PepperoniV2/Service/FirestoreService.swift index 93f0f55..d3ff3fd 100644 --- a/PepperoniV2/Service/FirestoreService.swift +++ b/PepperoniV2/Service/FirestoreService.swift @@ -6,6 +6,7 @@ // import SwiftData +import SwiftUI import Firebase import FirebaseFirestore import FirebaseStorage @@ -15,110 +16,115 @@ class FirestoreService { let storage = Storage.storage() let syncKey = "isDataSynced" - /// Firestore에서 데이터를 가져와 로컬 저장소와 SwiftData에 저장 + /// Firestore에서 anime의 타이틀만 불러와서 SwiftData 저장 @MainActor - func fetchAndStoreData(context: ModelContext) async throws { + func fetchAnimeTitles(context: ModelContext) async throws { // Firestore 컬렉션 경로 설정 let animeCollectionPath = "Anime" let animeSnapshot = try await db.collection(animeCollectionPath).getDocuments() - - var animeList: [Anime] = [] - + + var newAnimeTitles: [String] = [] + + // Firestore에서 모든 애니 제목 가져오기 for animeDocument in animeSnapshot.documents { let animeData = animeDocument.data() let animeID = animeDocument.documentID - + guard let animeTitle = animeData["animeTitle"] as? String else { print("Missing animeTitle in document: \(animeDocument.documentID)") continue } + + // SwiftData에 이미 저장된 애니인지 확인 + if context.fetch(Anime.self).first(where: { $0.id == animeID }) == nil { + // 새로운 Anime 객체 생성 및 SwiftData에 추가 + let newAnime = Anime(id: animeID, title: animeTitle, quotes: []) + context.insert(newAnime) + newAnimeTitles.append(animeTitle) + } + } + + do { + try context.save() + print("Successfully saved new anime titles to SwiftData.") + } catch { + print("Error saving data to SwiftData: \(error.localizedDescription)") + } - print("Fetching quotes for Anime ID: \(animeID)") - - let quotesPath = "\(animeCollectionPath)/\(animeID)/quotes" - let quotesSnapshot = try await db.collection(quotesPath).getDocuments() - - var quotes: [AnimeQuote] = [] - - for quoteDocument in quotesSnapshot.documents { - let quoteData = quoteDocument.data() - let quoteID = quoteDocument.documentID - - guard let japanese = quoteData["japanese"] as? [String], - let korean = quoteData["korean"] as? [String], - let audioFile = quoteData["audioFile"] as? String else { - print("Missing required fields in quote document: \(quoteDocument.documentID)") - continue - } - - let localFilePath = FileManager.default - .urls(for: .documentDirectory, in: .userDomainMask)[0] - .appendingPathComponent(audioFile).path - - let storagePath = "Animes/\(animeID)/\(audioFile)" - let shouldUpdate = try await shouldUpdateFile(filePath: localFilePath, storagePath: storagePath) + // TODO: 확인용, 제거 요망 + DispatchQueue.main.async { + if newAnimeTitles.isEmpty { + print("No new anime titles to add.") + } else { + print("New anime titles: \(newAnimeTitles)") + } + } + } - // 슈드업데이트 일 때만 해도 되는 동작들 - do { - if shouldUpdate { - try await downloadAudioFile(storagePath: storagePath, localPath: localFilePath) - - let quote = AnimeQuote( - id: quoteID, - japanese: japanese, - pronunciation: quoteData["pronunciation"] as? [String] ?? [], - korean: korean, - timeMark: quoteData["timeMark"] as? [Double] ?? [], - voicingTime: quoteData["voicingTime"] as? Double ?? 0.0, - audioFile: localFilePath, - youtubeID: quoteData["youtubeID"] as? String ?? "", - youtubeStartTime: quoteData["youtubeStartTime"] as? Double ?? 0.0, - youtubeEndTime: quoteData["youtubeEndTime"] as? Double ?? 0.0 - ) - quotes.append(quote) - } - }catch { - print("Not Should Update") - } - + /// 사용자가 선택한 애니 데이터를 Firestore에서 불러와 SwiftData와 로컬 저장소에 저장 + @MainActor + func fetchAnimeDetailsAndStore(context: ModelContext, animeID: String) async throws { + let animeCollectionPath = "Anime" + let animeDocumentPath = "\(animeCollectionPath)/\(animeID)" + let quotesPath = "\(animeDocumentPath)/quotes" + + let quotesSnapshot = try await db.collection(quotesPath).getDocuments() + var quotes: [AnimeQuote] = [] + + for quoteDocument in quotesSnapshot.documents { + let quoteData = quoteDocument.data() + let quoteID = quoteDocument.documentID + + guard let japanese = quoteData["japanese"] as? [String], + let korean = quoteData["korean"] as? [String], + let audioFile = quoteData["audioFile"] as? String else { + print("Missing required fields in quote document: \(quoteDocument.documentID)") + continue } - //quotes가 0개이면 돌아가지 않아야 함. anmieList에 빈 quotes가 비어있는 Anime를 넣는 것 방지. - if quotes.count > 0 { - let anime = Anime( - id: animeID, - title: animeTitle, - quotes: quotes - ) - animeList.append(anime) + let localFilePath = FileManager.default + .urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent(audioFile).path + + let storagePath = "Animes/\(animeID)/\(audioFile)" + let shouldUpdate = try await shouldUpdateFile(filePath: localFilePath, storagePath: storagePath) + + if shouldUpdate { + try await downloadAudioFile(storagePath: storagePath, localPath: localFilePath) } + + let quote = AnimeQuote( + id: quoteID, + japanese: japanese, + pronunciation: quoteData["pronunciation"] as? [String] ?? [], + korean: korean, + timeMark: quoteData["timeMark"] as? [Double] ?? [], + voicingTime: quoteData["voicingTime"] as? Double ?? 0.0, + audioFile: localFilePath, + youtubeID: quoteData["youtubeID"] as? String ?? "", + youtubeStartTime: quoteData["youtubeStartTime"] as? Double ?? 0.0, + youtubeEndTime: quoteData["youtubeEndTime"] as? Double ?? 0.0 + ) + quotes.append(quote) } - - // 로컬 데이터를 최신 상태로 유지 - // animeList가 1개 이상일 때만 돌아감 - if animeList.count > 0 { - DispatchQueue.main.async { - animeList.forEach { anime in - if let existingAnime = context.fetch(Anime.self).first(where: { $0.id == anime.id }) { - anime.quotes.forEach { newQuote in - if !existingAnime.quotes.contains(where: { $0.id == newQuote.id }) { - existingAnime.quotes.append(newQuote) - } - } - } else { - context.insert(anime) - } - } - do { - try context.save() - print("Data successfully saved to SwiftData.") - } catch { - print("Error saving data to SwiftData: \(error.localizedDescription)") + + // swiftdata의 anime와 firebase에서 불러온 anime를 비교, 없으면 swiftdata에 넣어줌 + if let existingAnime = context.fetch(Anime.self).first(where: { $0.id == animeID }) { + quotes.forEach { newQuote in + if !existingAnime.quotes.contains(where: { $0.id == newQuote.id }) { + existingAnime.quotes.append(newQuote) } } } + + do { + try context.save() + print("Successfully updated anime details and quotes to SwiftData.") + } catch { + print("Error saving details to SwiftData: \(error.localizedDescription)") + } } - + /// Firebase Storage 파일 업데이트 확인 및 다운로드 func shouldUpdateFile(filePath: String, storagePath: String) async throws -> Bool { let storageRef = storage.reference().child(storagePath) From 66d1f4ad70d679e16cf9af92e40a25ef915b907f Mon Sep 17 00:00:00 2001 From: HyunJaeyeon Date: Thu, 28 Nov 2024 17:47:45 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20#76-selectAnime=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 애니메를 선택했을 때 fetchAnimeDetailAndStore을 실행시킴 --- .../AnimeSelect/AnimeSelectView.swift | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/PepperoniV2/Presentation/GameSetting/AnimeSelect/AnimeSelectView.swift b/PepperoniV2/Presentation/GameSetting/AnimeSelect/AnimeSelectView.swift index 76e4d42..0af5eb9 100644 --- a/PepperoniV2/Presentation/GameSetting/AnimeSelect/AnimeSelectView.swift +++ b/PepperoniV2/Presentation/GameSetting/AnimeSelect/AnimeSelectView.swift @@ -9,11 +9,14 @@ import SwiftUI import SwiftData struct AnimeSelectView: View { + @Environment(\.modelContext) private var modelContext: ModelContext @Binding var isPresented: Bool @Environment(FetchDataState.self) var fetchDataState @Bindable var viewModel: AnimeSelectViewModel @Environment(GameViewModel.self) var gameViewModel @State private var searchText: String = "" + @State private var isLoading = false + private let firestoreService = FirestoreService() // SwiftData에서 Anime 데이터를 가져오기 @Query var animes: [Anime] @@ -62,7 +65,7 @@ struct AnimeSelectView: View { .padding(.horizontal, 16) // MARK: -ProgressView - if fetchDataState.isFetchingData { + if fetchDataState.isFetchingData || isLoading { HStack { Spacer() ProgressView("명대사를 불러오는 중...") @@ -86,7 +89,9 @@ struct AnimeSelectView: View { .listRowSeparator(.hidden) .listRowBackground(Color.clear) .onTapGesture { - viewModel.selectAnime(anime) + Task { + await selectAnime(anime) // 선택된 애니 데이터 로드 + } } } .listStyle(.plain) @@ -142,6 +147,19 @@ struct AnimeSelectView: View { } } } + + // 애니 선택 및 데이터 로드 + @MainActor + private func selectAnime(_ anime: Anime) async { + isLoading = true + do { + try await firestoreService.fetchAnimeDetailsAndStore(context: modelContext, animeID: anime.id) // modelContext 전달 + viewModel.selectAnime(anime) + } catch { + print("Failed to load anime details: \(error.localizedDescription)") + } + isLoading = false + } } struct DashLine: Shape { From 3cff9aa049b0269a20f511b5e97f47937e8d4d69 Mon Sep 17 00:00:00 2001 From: HyunJaeyeon Date: Thu, 28 Nov 2024 18:15:51 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20#76-=EC=95=B1=20=EC=8B=9C=EC=9E=91?= =?UTF-8?q?=20=EC=8B=9C=20title=EB=A7=8C=20=EB=B6=88=EB=9F=AC=EC=99=80=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PepperoniV2/App/PepperoniV2App.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PepperoniV2/App/PepperoniV2App.swift b/PepperoniV2/App/PepperoniV2App.swift index 2c30df6..656f855 100644 --- a/PepperoniV2/App/PepperoniV2App.swift +++ b/PepperoniV2/App/PepperoniV2App.swift @@ -38,7 +38,7 @@ struct PepperoniV2App: App { let context = modelContainer.mainContext Task { do { - try await FirestoreService().fetchAndStoreData(context: context) + try await FirestoreService().fetchAnimeTitles(context: context) fetchDataState.isFetchingData = false } catch { fetchDataState.errorMessage = error.localizedDescription From 60053c97a288dc9bf21a7fd8672ac5a990f56ee2 Mon Sep 17 00:00:00 2001 From: HyunJaeyeon Date: Thu, 28 Nov 2024 18:16:38 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20#76-=EC=9D=B4=EB=AF=B8=20quote?= =?UTF-8?q?=EA=B0=80=20=EC=A0=80=EC=9E=A5=EB=90=98=EC=96=B4=20=EC=9E=88?= =?UTF-8?q?=EC=9C=BC=EB=A9=B4=20=EB=B0=94=EB=A1=9C=20=EC=84=A0=ED=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GameSetting/AnimeSelect/AnimeSelectView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/PepperoniV2/Presentation/GameSetting/AnimeSelect/AnimeSelectView.swift b/PepperoniV2/Presentation/GameSetting/AnimeSelect/AnimeSelectView.swift index 0af5eb9..6e0524f 100644 --- a/PepperoniV2/Presentation/GameSetting/AnimeSelect/AnimeSelectView.swift +++ b/PepperoniV2/Presentation/GameSetting/AnimeSelect/AnimeSelectView.swift @@ -151,6 +151,12 @@ struct AnimeSelectView: View { // 애니 선택 및 데이터 로드 @MainActor private func selectAnime(_ anime: Anime) async { + // quotes가 이미 저장되어 있으면 바로 선택 + if !anime.quotes.isEmpty { + viewModel.selectAnime(anime) + return + } + isLoading = true do { try await firestoreService.fetchAnimeDetailsAndStore(context: modelContext, animeID: anime.id) // modelContext 전달