Skip to content

Commit

Permalink
Merge pull request #99 from DeveloperAcademy-POSTECH/feat/#76-AnimeDo…
Browse files Browse the repository at this point in the history
…wnload

[Feat]#76-anime 다운로드 방식 수정 (1차)
  • Loading branch information
SSub-jun authored Nov 30, 2024
2 parents 5fa73af + 6b38b77 commit e460b87
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 87 deletions.
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) // 선택된 애니 데이터 로드
}
}
.padding(.bottom, index == currentAnimes.count - 1 ? 60 : 0)
}
Expand Down Expand Up @@ -144,6 +148,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

0 comments on commit e460b87

Please sign in to comment.