Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat]#76-anime 다운로드 방식 수정 (1차) #99

Merged
merged 6 commits into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PepperoniV2/App/PepperoniV2App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -62,7 +65,7 @@ struct AnimeSelectView: View {
.padding(.horizontal, 16)

// MARK: -ProgressView
if fetchDataState.isFetchingData {
if fetchDataState.isFetchingData || isLoading {
HStack {
Spacer()
ProgressView("명대사를 불러오는 중...")
Expand All @@ -86,8 +89,9 @@ struct AnimeSelectView: View {
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.onTapGesture {
viewModel.selectAnime(anime)
HapticManager.instance.impact(style: .light)
Task {
await selectAnime(anime) // 선택된 애니 데이터 로드
}
}
}
.listStyle(.plain)
Expand Down Expand Up @@ -143,6 +147,25 @@ 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 전달
viewModel.selectAnime(anime)
} catch {
print("Failed to load anime details: \(error.localizedDescription)")
}
isLoading = false
}
}

struct DashLine: Shape {
Expand Down
172 changes: 89 additions & 83 deletions PepperoniV2/Service/FirestoreService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftData
import SwiftUI
import Firebase
import FirebaseFirestore
import FirebaseStorage
Expand All @@ -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)
Expand Down