diff --git a/WhereAreYou/WhereAreYou.xcodeproj/project.pbxproj b/WhereAreYou/WhereAreYou.xcodeproj/project.pbxproj
index 4b5589a..d85fd05 100644
--- a/WhereAreYou/WhereAreYou.xcodeproj/project.pbxproj
+++ b/WhereAreYou/WhereAreYou.xcodeproj/project.pbxproj
@@ -179,9 +179,11 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = WhereAreYou/WhereAreYou.entitlements;
- CODE_SIGN_STYLE = Automatic;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 2ZF894QLGK;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 2ZF894QLGK;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = WhereAreYou/Core/Info.plist;
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "약속이 진행되는 동안 상대방에게 실시간 위치를 공유하기 위해 위치 정보가 필요해요.";
@@ -198,6 +200,8 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = kr.syl.WhereAreYou;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = whereareyou;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@@ -220,9 +224,11 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = WhereAreYou/WhereAreYou.entitlements;
- CODE_SIGN_STYLE = Automatic;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 2ZF894QLGK;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 2ZF894QLGK;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = WhereAreYou/Core/Info.plist;
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "약속이 진행되는 동안 상대방에게 실시간 위치를 공유하기 위해 위치 정보가 필요해요.";
@@ -239,6 +245,8 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = kr.syl.WhereAreYou;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = whereareyou;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
diff --git a/WhereAreYou/WhereAreYou.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/WhereAreYou/WhereAreYou.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/WhereAreYou/WhereAreYou.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/WhereAreYou/WhereAreYou/Core/AppDelegate.swift b/WhereAreYou/WhereAreYou/Core/AppDelegate.swift
index 96ee880..ad4420d 100644
--- a/WhereAreYou/WhereAreYou/Core/AppDelegate.swift
+++ b/WhereAreYou/WhereAreYou/Core/AppDelegate.swift
@@ -39,7 +39,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
firestoreSettings.cacheSettings = MemoryCacheSettings()
Firestore.firestore().settings = firestoreSettings
Database.database().useEmulator(withHost: host, port: 9000)
- Storage.storage().useEmulator(withHost: host, port: 9199)
#endif
}
diff --git a/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift b/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift
index 474d391..c76ccee 100644
--- a/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift
+++ b/WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift
@@ -22,6 +22,9 @@ extension DIContainer {
}
))
+ // 프로필 이미지
+ register(ProfileImageRepository.self, instance: StorageProfileImageRepository())
+
// 위치
let coreLocationRepository = CoreLocationRepository()
register(LocationRepository.self, instance: coreLocationRepository)
diff --git a/WhereAreYou/WhereAreYou/Data/FirestoreUserRepository.swift b/WhereAreYou/WhereAreYou/Data/FirestoreUserRepository.swift
index f014b9a..0389ed9 100644
--- a/WhereAreYou/WhereAreYou/Data/FirestoreUserRepository.swift
+++ b/WhereAreYou/WhereAreYou/Data/FirestoreUserRepository.swift
@@ -33,7 +33,6 @@ final class FirestoreUserRepository: UserRepository {
let userData: [String: Any] = [
"nickname": nickname,
- // TODO: Firebase Storage 전환 시 실제 URL로 교체
"profileImage": "basic",
"defaultTransportMode": "TRANSIT",
"locationSharingScope": "ONLY_DURING_APPOINTMENT",
@@ -47,8 +46,7 @@ final class FirestoreUserRepository: UserRepository {
return User(
id: userID,
nickname: nickname,
- // TODO: Firebase Storage 전환 시 실제 URL로 교체
- profileImage: URL(string: "basic")!,
+ profileImage: "basic",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
diff --git a/WhereAreYou/WhereAreYou/Data/MockAppointmentDetailRepository.swift b/WhereAreYou/WhereAreYou/Data/MockAppointmentDetailRepository.swift
index ca1297c..c8af7eb 100644
--- a/WhereAreYou/WhereAreYou/Data/MockAppointmentDetailRepository.swift
+++ b/WhereAreYou/WhereAreYou/Data/MockAppointmentDetailRepository.swift
@@ -36,10 +36,6 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository {
private static let currentUserID = "user_me"
private static let mockCode = "MOCK1234"
- private static func profileImageURL(_ assetName: String) -> URL {
- URL(string: "asset://\(assetName)")!
- }
-
private static func makeMockAppointment(appointmentID: String) -> Appointment {
let place = Place(
id: "place_starbucks_gangnam",
@@ -55,7 +51,7 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository {
User(
id: currentUserID,
nickname: "나",
- profileImage: profileImageURL("shark"),
+ profileImage: "shark",
defaultTransportMode: .car,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -64,7 +60,7 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository {
User(
id: "user_minji",
nickname: "김민지",
- profileImage: profileImageURL("turtle"),
+ profileImage: "turtle",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -73,7 +69,7 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository {
User(
id: "user_seoyeon",
nickname: "이서연",
- profileImage: profileImageURL("shark"),
+ profileImage: "shark",
defaultTransportMode: .car,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -82,7 +78,7 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository {
User(
id: "user_junwoo",
nickname: "박준우",
- profileImage: profileImageURL("turtle"),
+ profileImage: "turtle",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -91,7 +87,7 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository {
User(
id: "user_gildong",
nickname: "최길동",
- profileImage: profileImageURL("shark"),
+ profileImage: "shark",
defaultTransportMode: .walk,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -100,7 +96,7 @@ final class MockAppointmentDetailRepository: AppointmentDetailRepository {
User(
id: "user_subin",
nickname: "정수빈",
- profileImage: profileImageURL("turtle"),
+ profileImage: "turtle",
defaultTransportMode: .car,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
diff --git a/WhereAreYou/WhereAreYou/Data/MockAppointmentInfoRepository.swift b/WhereAreYou/WhereAreYou/Data/MockAppointmentInfoRepository.swift
index aae377c..5f0c98d 100644
--- a/WhereAreYou/WhereAreYou/Data/MockAppointmentInfoRepository.swift
+++ b/WhereAreYou/WhereAreYou/Data/MockAppointmentInfoRepository.swift
@@ -56,7 +56,7 @@ final class MockAppointmentInfoRepository: AppointmentInfoRepository {
private static let currentUser = User(
id: "me",
nickname: "나",
- profileImage: URL(string: "otter")!,
+ profileImage: "otter",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -66,7 +66,7 @@ final class MockAppointmentInfoRepository: AppointmentInfoRepository {
private static let user2 = User(
id: "user2",
nickname: "김길동",
- profileImage: URL(string: "shark")!,
+ profileImage: "shark",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -76,7 +76,7 @@ final class MockAppointmentInfoRepository: AppointmentInfoRepository {
private static let user3 = User(
id: "user3",
nickname: "홍길동",
- profileImage: URL(string: "turtle")!,
+ profileImage: "turtle",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
diff --git a/WhereAreYou/WhereAreYou/Data/MockChatRepository.swift b/WhereAreYou/WhereAreYou/Data/MockChatRepository.swift
index 31418c4..f320dac 100644
--- a/WhereAreYou/WhereAreYou/Data/MockChatRepository.swift
+++ b/WhereAreYou/WhereAreYou/Data/MockChatRepository.swift
@@ -66,7 +66,7 @@ final class MockChatRepository: ChatRepository {
private static let currentUser = User(
id: currentUserID,
nickname: "나",
- profileImage: URL(string: "https://placeholder")!,
+ profileImage: "otter",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -76,7 +76,7 @@ final class MockChatRepository: ChatRepository {
private static let user2 = User(
id: "user2",
nickname: "김길동",
- profileImage: URL(string: "shark")!,
+ profileImage: "shark",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -86,7 +86,7 @@ final class MockChatRepository: ChatRepository {
private static let user3 = User(
id: "user3",
nickname: "홍길동",
- profileImage: URL(string: "turtle")!,
+ profileImage: "turtle",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
diff --git a/WhereAreYou/WhereAreYou/Data/MockSharedPlaceRepository.swift b/WhereAreYou/WhereAreYou/Data/MockSharedPlaceRepository.swift
index a1b8e78..7a5e52c 100644
--- a/WhereAreYou/WhereAreYou/Data/MockSharedPlaceRepository.swift
+++ b/WhereAreYou/WhereAreYou/Data/MockSharedPlaceRepository.swift
@@ -17,7 +17,7 @@ final class MockSharedPlaceRepository: SharedPlaceRepository {
private static let currentUser = User(
id: currentUserID,
nickname: "나",
- profileImage: URL(string: "https://placeholder")!,
+ profileImage: "otter",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -27,7 +27,7 @@ final class MockSharedPlaceRepository: SharedPlaceRepository {
private static let user2 = User(
id: "user2",
nickname: "김길동",
- profileImage: URL(string: "shark")!,
+ profileImage: "shark",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
@@ -37,7 +37,7 @@ final class MockSharedPlaceRepository: SharedPlaceRepository {
private static let user3 = User(
id: "user3",
nickname: "홍길동",
- profileImage: URL(string: "turtle")!,
+ profileImage: "turtle",
defaultTransportMode: .transit,
locationSharingScope: .onlyDuringAppointment,
isNotificationEnabled: true,
diff --git a/WhereAreYou/WhereAreYou/Data/StorageProfileImageRepository.swift b/WhereAreYou/WhereAreYou/Data/StorageProfileImageRepository.swift
new file mode 100644
index 0000000..8f59d62
--- /dev/null
+++ b/WhereAreYou/WhereAreYou/Data/StorageProfileImageRepository.swift
@@ -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()
+
+ 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
+ }
+ }
+
+ 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
+ }
+
+}
diff --git a/WhereAreYou/WhereAreYou/Data/UserDTO.swift b/WhereAreYou/WhereAreYou/Data/UserDTO.swift
index 73d2cda..f4a3bf2 100644
--- a/WhereAreYou/WhereAreYou/Data/UserDTO.swift
+++ b/WhereAreYou/WhereAreYou/Data/UserDTO.swift
@@ -50,8 +50,7 @@ struct UserDTO {
return User(
id: id,
nickname: nickname,
- // TODO: Firebase Storage 전환 시 실제 URL로 교체
- profileImage: URL(string: profileImage)!,
+ profileImage: profileImage,
defaultTransportMode: transportMode,
locationSharingScope: sharingScope,
isNotificationEnabled: isNotificationEnabled,
diff --git a/WhereAreYou/WhereAreYou/Domain/Entity/User.swift b/WhereAreYou/WhereAreYou/Domain/Entity/User.swift
index abf6fd9..f23079a 100644
--- a/WhereAreYou/WhereAreYou/Domain/Entity/User.swift
+++ b/WhereAreYou/WhereAreYou/Domain/Entity/User.swift
@@ -11,7 +11,7 @@ struct User: Hashable {
let id: String
let nickname: String
- let profileImage: URL
+ let profileImage: String
let defaultTransportMode: TransportType
let locationSharingScope: LocationSharingScope
let isNotificationEnabled: Bool
diff --git a/WhereAreYou/WhereAreYou/Domain/Repository/ProfileImageRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/ProfileImageRepository.swift
new file mode 100644
index 0000000..5be9256
--- /dev/null
+++ b/WhereAreYou/WhereAreYou/Domain/Repository/ProfileImageRepository.swift
@@ -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
+
+}
diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/ParticipantBox.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/ParticipantBox.swift
index fe710b5..3e7607a 100644
--- a/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/ParticipantBox.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/Chat/AppointmentInfo/ParticipantBox.swift
@@ -16,7 +16,8 @@ final class ParticipantBox: UIView {
init(member: Participant) {
super.init(frame: .zero)
- let avatar = UIImageView(image: UIImage(named: member.profileImage))
+ let avatar = UIImageView()
+ avatar.setProfileImage(member.profileImage)
avatar.backgroundColor = .pointBackground
avatar.layer.cornerRadius = Self.avatarSize / 2
avatar.clipsToBounds = true
diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/ChatViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/ChatViewModel.swift
index ba99d68..88410b0 100644
--- a/WhereAreYou/WhereAreYou/Presentation/Chat/ChatViewModel.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/Chat/ChatViewModel.swift
@@ -162,7 +162,7 @@ private extension ChatViewModel {
id: message.id,
senderID: message.sender.id,
senderNickname: message.sender.nickname,
- senderProfileImage: message.sender.profileImageName,
+ senderProfileImage: message.sender.profileImage,
content: bubbleContent,
timeText: timeText,
sentAt: message.sentAt,
diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/SharedPlace/SharedPlaceRow.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/SharedPlace/SharedPlaceRow.swift
index 975bb41..e51e8a7 100644
--- a/WhereAreYou/WhereAreYou/Presentation/Chat/SharedPlace/SharedPlaceRow.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/Chat/SharedPlace/SharedPlaceRow.swift
@@ -73,7 +73,8 @@ final class SharedPlaceRow: UIView {
private func setUpVoterImages(_ images: [String]) {
for imageName in images {
- let imageView = UIImageView(image: UIImage(named: imageName))
+ let imageView = UIImageView()
+ imageView.setProfileImage(imageName)
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = Self.profileSize / 2
imageView.clipsToBounds = true
diff --git a/WhereAreYou/WhereAreYou/Presentation/Chat/SubComponent/OtherChatBubble.swift b/WhereAreYou/WhereAreYou/Presentation/Chat/SubComponent/OtherChatBubble.swift
index 90ae3b5..78236fe 100644
--- a/WhereAreYou/WhereAreYou/Presentation/Chat/SubComponent/OtherChatBubble.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/Chat/SubComponent/OtherChatBubble.swift
@@ -83,7 +83,8 @@ final class OtherChatBubble: ChatBubbleBase {
}
private func makeAvatarView(imageName: String) -> UIImageView {
- let imageView = UIImageView(image: UIImage(named: imageName))
+ let imageView = UIImageView()
+ imageView.setProfileImage(imageName)
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = Self.avatarSize / 2
imageView.clipsToBounds = true
diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/ProfileImageLoader.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/ProfileImageLoader.swift
new file mode 100644
index 0000000..c085f58
--- /dev/null
+++ b/WhereAreYou/WhereAreYou/Presentation/Extension/ProfileImageLoader.swift
@@ -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()
+ }()
+
+}
diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/UIImageView+ProfileImage.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/UIImageView+ProfileImage.swift
new file mode 100644
index 0000000..c50b659
--- /dev/null
+++ b/WhereAreYou/WhereAreYou/Presentation/Extension/UIImageView+ProfileImage.swift
@@ -0,0 +1,60 @@
+//
+// UIImageView+ProfileImage.swift
+// WhereAreYou
+//
+// Created by 이상유 on 2026-09-13.
+//
+
+import UIKit
+import ObjectiveC
+
+/// UIImageView에서 프로필 아바타 식별자로 Storage 이미지를 비동기 로딩하는 extension
+extension UIImageView {
+
+ func setProfileImage(_ identifier: String) {
+ profileLoadTask?.cancel()
+ indicatorDelayTask?.cancel()
+ currentProfileIdentifier = identifier
+ image = nil
+ hideLoadingIndicator()
+
+ indicatorDelayTask = Task { [weak self] in
+ try await Task.sleep(for: .milliseconds(150))
+ self?.showLoadingIndicator()
+ }
+
+ let showTask = indicatorDelayTask
+ profileLoadTask = Task { [weak self] in
+ let loaded = await ProfileImageLoader.shared.load(identifier: identifier)
+ showTask?.cancel()
+ guard !Task.isCancelled else { return }
+ guard self?.currentProfileIdentifier == identifier else { return }
+ self?.hideLoadingIndicator()
+ self?.image = loaded
+ }
+ }
+
+}
+
+private var profileIdentifierKey: UInt8 = 0
+private var profileLoadTaskKey: UInt8 = 0
+private var indicatorDelayTaskKey: UInt8 = 0
+
+private extension UIImageView {
+
+ var currentProfileIdentifier: String? {
+ get { objc_getAssociatedObject(self, &profileIdentifierKey) as? String }
+ set { objc_setAssociatedObject(self, &profileIdentifierKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
+ }
+
+ var profileLoadTask: Task? {
+ get { objc_getAssociatedObject(self, &profileLoadTaskKey) as? Task }
+ set { objc_setAssociatedObject(self, &profileLoadTaskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
+ }
+
+ var indicatorDelayTask: Task? {
+ get { objc_getAssociatedObject(self, &indicatorDelayTaskKey) as? Task }
+ set { objc_setAssociatedObject(self, &indicatorDelayTaskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
+ }
+
+}
diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/UIView+LoadingIndicator.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/UIView+LoadingIndicator.swift
new file mode 100644
index 0000000..0c48160
--- /dev/null
+++ b/WhereAreYou/WhereAreYou/Presentation/Extension/UIView+LoadingIndicator.swift
@@ -0,0 +1,49 @@
+//
+// UIView+LoadingIndicator.swift
+// WhereAreYou
+//
+// Created by 이상유 on 2026-09-14.
+//
+
+import UIKit
+import ObjectiveC
+
+/// UIView에 로딩 인디케이터를 표시·제거하는 extension
+extension UIView {
+
+ func showLoadingIndicator(style: UIActivityIndicatorView.Style = .medium, color: UIColor? = nil) {
+ guard loadingIndicator == nil else { return }
+
+ let indicator = UIActivityIndicatorView(style: style)
+ indicator.translatesAutoresizingMaskIntoConstraints = false
+ indicator.hidesWhenStopped = true
+ if let color { indicator.color = color }
+ addSubview(indicator)
+
+ NSLayoutConstraint.activate([
+ indicator.centerXAnchor.constraint(equalTo: centerXAnchor),
+ indicator.centerYAnchor.constraint(equalTo: centerYAnchor)
+ ])
+
+ indicator.startAnimating()
+ loadingIndicator = indicator
+ }
+
+ func hideLoadingIndicator() {
+ loadingIndicator?.stopAnimating()
+ loadingIndicator?.removeFromSuperview()
+ loadingIndicator = nil
+ }
+
+}
+
+private var loadingIndicatorKey: UInt8 = 0
+
+private extension UIView {
+
+ var loadingIndicator: UIActivityIndicatorView? {
+ get { objc_getAssociatedObject(self, &loadingIndicatorKey) as? UIActivityIndicatorView }
+ set { objc_setAssociatedObject(self, &loadingIndicatorKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
+ }
+
+}
diff --git a/WhereAreYou/WhereAreYou/Presentation/Extension/User+ProfileImage.swift b/WhereAreYou/WhereAreYou/Presentation/Extension/User+ProfileImage.swift
deleted file mode 100644
index 3ea6cae..0000000
--- a/WhereAreYou/WhereAreYou/Presentation/Extension/User+ProfileImage.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-//
-// User+ProfileImage.swift
-// WhereAreYou
-//
-// Created by 이상유 on 2026-08-17.
-//
-
-import Foundation
-
-extension User {
-
- /// 프로필 이미지 URL에서 화면에 사용할 에셋 이름만 추출
- /// - 서버 연동 후 이미지 표현이 바뀌면 이 프로퍼티 한 곳만 수정하면 된다.
- var profileImageName: String {
- profileImage.lastPathComponent
- }
-
-}
diff --git a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteMapView.swift b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteMapView.swift
index 466a89e..a9ce85f 100644
--- a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteMapView.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/AppointmentRouteMapView.swift
@@ -13,6 +13,7 @@ final class AppointmentRouteMapView: NMFNaverMapView {
private var placeMarker: NMFMarker?
private var participantMarkers: [NMFMarker] = []
private var participantPolylines: [NMFPolylineOverlay] = []
+ private var markerLoadTask: Task?
private var hasMovedToInitialPosition = false
override init(frame: CGRect) {
@@ -57,12 +58,21 @@ final class AppointmentRouteMapView: NMFNaverMapView {
}
func setParticipants(_ participants: [AppointmentRouteParticipant]) {
+ markerLoadTask?.cancel()
clearParticipantOverlays()
let colors = UIColor.participantColors(count: participants.count)
for (index, participant) in participants.enumerated() {
addPolyline(for: participant, color: colors[index])
- addMarker(for: participant, color: colors[index])
+ }
+
+ markerLoadTask = Task { [weak self] in
+ let images = await self?.loadProfileImages(for: participants) ?? []
+ guard !Task.isCancelled, !images.isEmpty else { return }
+
+ for (index, participant) in participants.enumerated() {
+ self?.addMarker(for: participant, color: colors[index], profileImage: images[index])
+ }
}
}
@@ -97,13 +107,27 @@ private extension AppointmentRouteMapView {
participantPolylines.append(overlay)
}
- func addMarker(for participant: AppointmentRouteParticipant, color: UIColor) {
+ func loadProfileImages(for participants: [AppointmentRouteParticipant]) async -> [UIImage] {
+ await withTaskGroup(of: (Int, UIImage).self) { group in
+ for (index, participant) in participants.enumerated() {
+ group.addTask {
+ let image = await ProfileImageLoader.shared.load(identifier: participant.profileImage)
+ return (index, image)
+ }
+ }
+
+ var images = [UIImage](repeating: ProfileImageLoader.failureImage, count: participants.count)
+ for await (index, image) in group {
+ images[index] = image
+ }
+ return images
+ }
+ }
+
+ func addMarker(for participant: AppointmentRouteParticipant, color: UIColor, profileImage: UIImage) {
guard let position = participant.path.first else { return }
- let kind = MapMarker.Kind.participant(
- profileImage: UIImage(named: participant.profileImageURL.host ?? ""),
- tintColor: color
- )
+ let kind = MapMarker.Kind.participant(profileImage: profileImage, tintColor: color)
let markerImage = MapMarker.renderImage(kind: kind, name: participant.nickname)
let marker = NMFMarker(position: NMGLatLng(lat: position.latitude, lng: position.longitude))
diff --git a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/ParticipantRouteRow.swift b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/ParticipantRouteRow.swift
index ff767b1..b789455 100644
--- a/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/ParticipantRouteRow.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/Map/AppointmentRoute/ParticipantRouteRow.swift
@@ -32,7 +32,8 @@ final class ParticipantRouteRow: UIView {
avatarContainer.heightAnchor.constraint(equalToConstant: Self.avatarDiameter).isActive = true
let avatarInset = Self.avatarDiameter * 0.15
- let avatar = UIImageView(image: UIImage(named: participant.profileImageURL.host ?? ""))
+ let avatar = UIImageView()
+ avatar.setProfileImage(participant.profileImage)
avatar.contentMode = .scaleAspectFit
avatar.translatesAutoresizingMaskIntoConstraints = false
avatarContainer.addSubview(avatar)
diff --git a/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/ProfileEditViewController.swift b/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/ProfileEditViewController.swift
index a9a64c7..336c801 100644
--- a/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/ProfileEditViewController.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/ProfileEditViewController.swift
@@ -14,16 +14,7 @@ final class ProfileEditViewController: UIViewController {
private static let profileImageCellSize: CGFloat = 90
private static let previewImageSize: CGFloat = 96
- private static let availableProfileImageNames: [String] = {
- let assetImageNames = ["shark", "turtle"]
- let symbolImageNames = [
- "pawprint.fill", "tortoise.fill", "hare.fill", "bird.fill", "fish.fill",
- "ladybug.fill", "ant.fill", "lizard.fill", "cat.fill", "dog.fill",
- "bee.fill", "butterfly.fill", "leaf.fill", "pawprint", "tortoise",
- "hare", "bird", "fish", "ladybug", "ant"
- ]
- return assetImageNames + symbolImageNames
- }()
+ private var availableImageNames: [String] = []
private var selectedImageName: String {
didSet { updatePreviewImage() }
@@ -43,7 +34,6 @@ final class ProfileEditViewController: UIViewController {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .pointBackground
- imageView.tintColor = .blue2
imageView.clipsToBounds = true
return imageView
}()
@@ -61,6 +51,8 @@ final class ProfileEditViewController: UIViewController {
field.layer.shadowOffset = CGSize(width: 0, height: 2)
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 0))
field.leftViewMode = .always
+ field.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 0))
+ field.rightViewMode = .always
field.returnKeyType = .done
field.enablesReturnKeyAutomatically = true
return field
@@ -106,8 +98,8 @@ final class ProfileEditViewController: UIViewController {
setUpLayout()
setUpDismissKeyboardGesture()
setUpActions()
- applySnapshot()
updatePreviewImage()
+ loadAvailableImages()
}
override func viewWillAppear(_ animated: Bool) {
@@ -184,13 +176,23 @@ final class ProfileEditViewController: UIViewController {
}
}
+ // MARK: - Data
+
+ private func loadAvailableImages() {
+ Task { [weak self] in
+ let names = (try? await ProfileImageLoader.shared.fetchAvailableImageNames()) ?? []
+ self?.availableImageNames = names
+ self?.applySnapshot()
+ }
+ }
+
private func applySnapshot() {
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([0])
- snapshot.appendItems(Self.availableProfileImageNames, toSection: 0)
+ snapshot.appendItems(availableImageNames, toSection: 0)
dataSource.apply(snapshot, animatingDifferences: false)
- if let selectedIndex = Self.availableProfileImageNames.firstIndex(of: selectedImageName) {
+ if let selectedIndex = availableImageNames.firstIndex(of: selectedImageName) {
collectionView.selectItem(
at: IndexPath(item: selectedIndex, section: 0),
animated: false,
@@ -200,7 +202,7 @@ final class ProfileEditViewController: UIViewController {
}
private func updatePreviewImage() {
- previewImageView.image = UIImage(named: selectedImageName) ?? UIImage(systemName: selectedImageName)
+ previewImageView.setProfileImage(selectedImageName)
previewImageView.layer.cornerRadius = Self.previewImageSize / 2
}
@@ -233,7 +235,7 @@ final class ProfileEditViewController: UIViewController {
extension ProfileEditViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
- selectedImageName = Self.availableProfileImageNames[indexPath.item]
+ selectedImageName = availableImageNames[indexPath.item]
}
}
diff --git a/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/ProfileImageCell.swift b/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/ProfileImageCell.swift
index ddac51f..33ca4a7 100644
--- a/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/ProfileImageCell.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/MyPage/Detail/ProfileImageCell.swift
@@ -39,8 +39,7 @@ final class ProfileImageCell: UICollectionViewCell {
}
func configure(imageName: String) {
- imageView.image = UIImage(named: imageName) ?? UIImage(systemName: imageName)
- imageView.tintColor = .blue2
+ imageView.setProfileImage(imageName)
}
private func setUp() {
diff --git a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageRow.swift b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageRow.swift
index 80900a9..3dc37de 100644
--- a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageRow.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageRow.swift
@@ -84,6 +84,10 @@ final class MyPageRow: UIControl {
iconView.image = newIcon
}
+ func setProfileIcon(_ identifier: String) {
+ iconView.setProfileImage(identifier)
+ }
+
private func setUp(accessoryView: UIView?, accessoryHasOwnInteraction: Bool) {
let iconSize: CGFloat = 22
iconView.widthAnchor.constraint(equalToConstant: iconSize).isActive = true
diff --git a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewController.swift b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewController.swift
index 1cfb83a..5dcfeee 100644
--- a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewController.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewController.swift
@@ -58,12 +58,16 @@ final class MyPageViewController: UIViewController {
imageView.widthAnchor.constraint(equalToConstant: 16).isActive = true
return imageView
}()
- private lazy var profileRow = MyPageRow(
- icon: UIImage(named: viewModel.profile.profileImageName) ?? UIImage(systemName: viewModel.profile.profileImageName),
- title: "닉네임",
- isCircularImage: true,
- accessoryView: profileEditIcon
- )
+ private lazy var profileRow: MyPageRow = {
+ let row = MyPageRow(
+ icon: nil,
+ title: "닉네임",
+ isCircularImage: true,
+ accessoryView: profileEditIcon
+ )
+ row.setProfileIcon(viewModel.profile.profileImageName)
+ return row
+ }()
// MARK: - 위치 카드
@@ -200,7 +204,7 @@ final class MyPageViewController: UIViewController {
let profile = viewModel.profile
profileRow.value = profile.nickname
- profileRow.setIcon(UIImage(named: profile.profileImageName) ?? UIImage(systemName: profile.profileImageName))
+ profileRow.setProfileIcon(profile.profileImageName)
locationSharingRow.value = profile.locationSharingOption.title
locationPermissionRow.value = viewModel.locationPermissionState.title
notificationSwitch.isOn = profile.isNotificationEnabled
diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentRouteParticipant.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentRouteParticipant.swift
index 97dc3ac..d7cbac9 100644
--- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentRouteParticipant.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/AppointmentRouteParticipant.swift
@@ -11,7 +11,7 @@ struct AppointmentRouteParticipant {
let id: String
let nickname: String
- let profileImageURL: URL
+ let profileImage: String
let isMe: Bool
let path: [Coordinate]
let transportName: String
@@ -28,7 +28,7 @@ struct AppointmentRouteParticipant {
let transport = route.step.last?.transportType ?? user.defaultTransportMode
id = user.id
nickname = user.nickname
- profileImageURL = user.profileImage
+ profileImage = user.profileImage
isMe = user.id == currentUserID
path = route.step.flatMap(\.path)
transportName = transport.name
diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/Participant.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/Participant.swift
index fc4b103..6dfea38 100644
--- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/Participant.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/Participant.swift
@@ -14,7 +14,7 @@ struct Participant {
init(user: User) {
id = user.id
nickname = user.nickname
- profileImage = user.profileImageName
+ self.profileImage = user.profileImage
}
init(id: String, nickname: String, profileImage: String) {
diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/SharedPlaceItem.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/SharedPlaceItem.swift
index 90ec618..8a97cc7 100644
--- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/SharedPlaceItem.swift
+++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/SharedPlaceItem.swift
@@ -21,7 +21,7 @@ struct SharedPlaceItem {
id = sharedPlace.id
placeName = sharedPlace.place.name
placeAddress = sharedPlace.place.address
- voterProfileImages = sharedPlace.voters.map { $0.profileImageName }
+ voterProfileImages = sharedPlace.voters.map(\.profileImage)
hasVoted = sharedPlace.voters.contains { $0.id == currentUserID }
sharedAt = sharedPlace.sharedAt
}
diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/Contents.json b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/Contents.json
deleted file mode 100644
index 73c0059..0000000
--- a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/otter.imageset/Contents.json b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/otter.imageset/Contents.json
deleted file mode 100644
index 828bfd4..0000000
--- a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/otter.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "otter.png",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/otter.imageset/otter.png b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/otter.imageset/otter.png
deleted file mode 100644
index 690c432..0000000
Binary files a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/otter.imageset/otter.png and /dev/null differ
diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/shark.imageset/Contents.json b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/shark.imageset/Contents.json
deleted file mode 100644
index eece125..0000000
--- a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/shark.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "shark.png",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/shark.imageset/shark.png b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/shark.imageset/shark.png
deleted file mode 100644
index 1c17833..0000000
Binary files a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/shark.imageset/shark.png and /dev/null differ
diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/turtle.imageset/Contents.json b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/turtle.imageset/Contents.json
deleted file mode 100644
index 73f8db3..0000000
--- a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/turtle.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "filename" : "turtle.png",
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/turtle.imageset/turtle.png b/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/turtle.imageset/turtle.png
deleted file mode 100644
index 4df0100..0000000
Binary files a/WhereAreYou/WhereAreYou/Resource/Assets.xcassets/Profile/turtle.imageset/turtle.png and /dev/null differ
diff --git a/firebase.json b/firebase.json
index 93b4c19..b260b85 100644
--- a/firebase.json
+++ b/firebase.json
@@ -2,12 +2,12 @@
"firestore": {
"database": "(default)",
"location": "asia-northeast3",
- "rules": "firestore.rules",
- "indexes": "firestore.indexes.json"
+ "rules": "firebase/firestore.rules",
+ "indexes": "firebase/firestore.indexes.json"
},
"functions": [
{
- "source": "functions",
+ "source": "firebase/functions",
"codebase": "default",
"disallowLegacyRuntimeConfig": true,
"ignore": [
@@ -20,7 +20,7 @@
}
],
"storage": {
- "rules": "storage.rules"
+ "rules": "firebase/storage.rules"
},
"emulators": {
"auth": {
@@ -45,7 +45,7 @@
"singleProjectMode": true
},
"database": {
- "rules": "database.rules.json"
+ "rules": "firebase/database.rules.json"
},
"auth": {
"providers": {}
diff --git a/database.rules.json b/firebase/database.rules.json
similarity index 100%
rename from database.rules.json
rename to firebase/database.rules.json
diff --git a/firestore.indexes.json b/firebase/firestore.indexes.json
similarity index 100%
rename from firestore.indexes.json
rename to firebase/firestore.indexes.json
diff --git a/firestore.rules b/firebase/firestore.rules
similarity index 100%
rename from firestore.rules
rename to firebase/firestore.rules
diff --git a/functions/.gitignore b/firebase/functions/.gitignore
similarity index 100%
rename from functions/.gitignore
rename to firebase/functions/.gitignore
diff --git a/functions/index.js b/firebase/functions/index.js
similarity index 100%
rename from functions/index.js
rename to firebase/functions/index.js
diff --git a/functions/package-lock.json b/firebase/functions/package-lock.json
similarity index 100%
rename from functions/package-lock.json
rename to firebase/functions/package-lock.json
diff --git a/functions/package.json b/firebase/functions/package.json
similarity index 100%
rename from functions/package.json
rename to firebase/functions/package.json
diff --git a/storage.rules b/firebase/storage.rules
similarity index 100%
rename from storage.rules
rename to firebase/storage.rules