-
Notifications
You must be signed in to change notification settings - Fork 0
[Work 52] 프로필 이미지를 Firebase Storage에서 가져오도록 했습니다. #22
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
base: develop
Are you sure you want to change the base?
Changes from all commits
5c23249
a73c80c
e2432e6
25cbcca
9bef298
a199ceb
1e0ad0e
1b9701c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // | ||
| // StorageProfileImageRepository.swift | ||
| // WhereAreYou | ||
| // | ||
| // Created by 이상유 on 2026-09-14. | ||
| // | ||
|
|
||
| import Foundation | ||
| import FirebaseStorage | ||
|
|
||
| /// Firebase Storage 기반 프로필 이미지 저장소 | ||
| actor StorageProfileImageRepository: ProfileImageRepository { | ||
|
|
||
| private let storage = Storage.storage() | ||
| private let maxImageSize: Int64 = 1 * 1024 * 1024 | ||
|
|
||
| private var cachedNames: [String]? | ||
| private var namesFetchTask: Task<[String], Error>? | ||
| private let dataCache = NSCache<NSString, NSData>() | ||
|
|
||
| func fetchAvailableImageNames() async throws -> [String] { | ||
| if let cached = cachedNames { | ||
| return cached | ||
| } | ||
|
|
||
| if let existing = namesFetchTask { | ||
| return try await existing.value | ||
| } | ||
|
|
||
| let storage = self.storage | ||
| let task = Task<[String], Error> { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH]
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Task 클로저 본문에서는 로컬 변수 storage만 캡처해 Firebase API 호출만 수행하며, cachedNames, namesFetchTask에 접근하지 않습니다. 해당 프로퍼티 변경은 Task 바깥의 본문에서 수행됩니다. 또한 Task 생성과 namesFetchTask = task 할당 사이에 suspension point가 없으므로, actor 직렬화에 의해 원자적으로 실행됩니다. 첫 suspension은 await task.value(40행)인데, 그 시점에는 이미 namesFetchTask가 설정되어 있어 후속 caller는 if let existing = namesFetchTask 분기를 타고 동일 Task를 공유합니다. 현재 구조에서 중복 네트워크 요청은 발생하지 않습니다. func fetchAvailableImageNames() async throws -> [String] {
if let cached = cachedNames {
return cached
}
if let existing = namesFetchTask {
return try await existing.value
}
let storage = self.storage
let task = Task<[String], Error> {
let result = try await storage.reference().child("avatars").listAll()
return result.items
.map { ($0.name as NSString).deletingPathExtension }
.sorted()
}
namesFetchTask = task
do {
let names = try await task.value
cachedNames = names
namesFetchTask = nil
return names
} catch {
namesFetchTask = nil
throw error
}
}
sangYuLv marked this conversation as resolved.
|
||
| let result = try await storage.reference().child("avatars").listAll() | ||
| return result.items | ||
| .map { ($0.name as NSString).deletingPathExtension } | ||
| .sorted() | ||
| } | ||
| namesFetchTask = task | ||
|
|
||
| do { | ||
| let names = try await task.value | ||
| cachedNames = names | ||
| namesFetchTask = nil | ||
| return names | ||
| } catch { | ||
| namesFetchTask = nil | ||
| throw error | ||
| } | ||
| } | ||
|
|
||
| func downloadImageData(identifier: String) async throws -> Data { | ||
| let key = identifier as NSString | ||
| if let cached = dataCache.object(forKey: key) { | ||
| return cached as Data | ||
| } | ||
|
|
||
| let ref = storage.reference().child("avatars/\(identifier).png") | ||
| let data = try await ref.data(maxSize: maxImageSize) | ||
| dataCache.setObject(data as NSData, forKey: key) | ||
| return data | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // | ||
| // ProfileImageRepository.swift | ||
| // WhereAreYou | ||
| // | ||
| // Created by 이상유 on 2026-09-14. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| /// 프로필 이미지 저장소 — 사용 가능한 이미지 목록 조회 및 이미지 데이터 다운로드 | ||
| protocol ProfileImageRepository { | ||
|
|
||
| func fetchAvailableImageNames() async throws -> [String] | ||
|
|
||
| func downloadImageData(identifier: String) async throws -> Data | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // | ||
| // ProfileImageLoader.swift | ||
| // WhereAreYou | ||
| // | ||
| // Created by 이상유 on 2026-09-13. | ||
| // | ||
|
|
||
| import UIKit | ||
|
|
||
| /// 프로필 이미지를 로드해 UIImage로 변환하는 싱글턴 로더 | ||
| final class ProfileImageLoader { | ||
|
|
||
| static let shared = ProfileImageLoader() | ||
|
|
||
| private let repository: ProfileImageRepository | ||
|
|
||
| private init() { | ||
| repository = DIContainer.shared.resolve(ProfileImageRepository.self) | ||
| } | ||
|
|
||
| func fetchAvailableImageNames() async throws -> [String] { | ||
| try await repository.fetchAvailableImageNames() | ||
| } | ||
|
|
||
| func load(identifier: String) async -> UIImage { | ||
| do { | ||
| let data = try await repository.downloadImageData(identifier: identifier) | ||
| return UIImage(data: data) ?? Self.failureImage | ||
| } catch { | ||
| return Self.failureImage | ||
| } | ||
| } | ||
|
|
||
| // MARK: - 실패 이미지 | ||
|
|
||
| static let failureImage: UIImage = { | ||
| let config = UIImage.SymbolConfiguration(pointSize: 40, weight: .light) | ||
| let symbol = UIImage(systemName: "questionmark.circle", withConfiguration: config) | ||
| return symbol?.withTintColor(.secondaryLabel, renderingMode: .alwaysOriginal) | ||
| ?? UIImage() | ||
| }() | ||
|
|
||
| } |
Uh oh!
There was an error while loading. Please reload this page.