Skip to content

Commit 41e90f3

Browse files
committed
refactor: 프로필 아바타에서 CacheableImage 제거
1 parent a1a3785 commit 41e90f3

6 files changed

Lines changed: 168 additions & 121 deletions

File tree

Application/DevLogPresentation/Sources/Common/Component/CacheableImage.swift

Lines changed: 0 additions & 115 deletions
This file was deleted.

Application/DevLogPresentation/Sources/Profile/ProfileView.swift

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,7 @@ struct ProfileView: View {
5757
ScrollView {
5858
LazyVStack(alignment: .leading, spacing: 16) {
5959
HStack {
60-
CacheableImage(url: coordinator.viewModel.state.avatarURL) {
61-
Image(systemName: "person.crop.circle.fill")
62-
.resizable()
63-
.scaledToFill()
64-
.foregroundStyle(Color(.systemGray2))
65-
}
60+
profileAvatarImage
6661
.frame(width: 60, height: 60)
6762
.cornerRadius(30)
6863
.foregroundStyle(Color.gray)
@@ -129,6 +124,21 @@ struct ProfileView: View {
129124
.toolbar { profileToolbarContent }
130125
}
131126

127+
@ViewBuilder
128+
private var profileAvatarImage: some View {
129+
if let data = coordinator.viewModel.state.avatarImageData?.data,
130+
let uiImage = UIImage(data: data) {
131+
Image(uiImage: uiImage)
132+
.resizable()
133+
.scaledToFill()
134+
} else {
135+
Image(systemName: "person.crop.circle.fill")
136+
.resizable()
137+
.scaledToFill()
138+
.foregroundStyle(Color(.systemGray2))
139+
}
140+
}
141+
132142
@ToolbarContentBuilder
133143
private var profileToolbarContent: some ToolbarContent {
134144
ToolbarItem(placement: .topBarTrailing) {

Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ final class ProfileViewCoordinator {
2121
self.container = container
2222
self.viewModel = ProfileViewModel(
2323
fetchUserDataUseCase: container.resolve(FetchUserDataUseCase.self),
24+
fetchProfileImageDataUseCase: container.resolve(FetchProfileImageDataUseCase.self),
2425
fetchTodosUseCase: container.resolve(FetchTodosUseCase.self),
2526
upsertStatusMessageUseCase: container.resolve(UpsertStatusMessageUseCase.self),
2627
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),

Application/DevLogPresentation/Sources/Profile/ProfileViewModel.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ import Combine
1010
import DevLogCore
1111
import DevLogDomain
1212

13+
struct ProfileAvatarImageData: Equatable {
14+
let id: Int
15+
let data: Data
16+
17+
static func == (
18+
lhs: ProfileAvatarImageData,
19+
rhs: ProfileAvatarImageData
20+
) -> Bool {
21+
lhs.id == rhs.id
22+
}
23+
}
24+
1325
@Observable
1426
final class ProfileViewModel: StorePattern {
1527
struct State: Equatable {
@@ -19,6 +31,7 @@ final class ProfileViewModel: StorePattern {
1931
var isLoading: Bool = false
2032
var statusMessage: String = ""
2133
var avatarURL: URL?
34+
var avatarImageData: ProfileAvatarImageData?
2235
var earliestQuarterStart: Date?
2336
var selectedQuarterStart: Date?
2437
var showQuarterPicker: Bool = false
@@ -41,6 +54,7 @@ final class ProfileViewModel: StorePattern {
4154
case tapResetStatusMessageButton
4255
case willUpdateStatusMessage
4356
case fetchUserData(UserProfile)
57+
case setAvatarImageData(URL, Data)
4458
case setActivityQuarter(
4559
quarterStart: Date,
4660
quarter: HeatmapQuarter,
@@ -60,13 +74,15 @@ final class ProfileViewModel: StorePattern {
6074

6175
enum SideEffect {
6276
case fetchUserData
77+
case fetchAvatarImageData(URL)
6378
case fetchActivityQuarter(Date)
6479
case updateStatusMessage(String)
6580
case updateHeatmapActivityKinds(Set<ActivityKind>)
6681
}
6782

6883
private(set) var state = State()
6984
private let fetchUserDataUseCase: FetchUserDataUseCase
85+
private let fetchProfileImageDataUseCase: FetchProfileImageDataUseCase
7086
private let fetchTodosUseCase: FetchTodosUseCase
7187
private let upsertStatusMessageUseCase: UpsertStatusMessageUseCase
7288
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
@@ -78,13 +94,15 @@ final class ProfileViewModel: StorePattern {
7894

7995
init(
8096
fetchUserDataUseCase: FetchUserDataUseCase,
97+
fetchProfileImageDataUseCase: FetchProfileImageDataUseCase,
8198
fetchTodosUseCase: FetchTodosUseCase,
8299
upsertStatusMessageUseCase: UpsertStatusMessageUseCase,
83100
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
84101
fetchHeatmapActivityTypesUseCase: FetchHeatmapActivityTypesUseCase,
85102
updateHeatmapActivityTypesUseCase: UpdateHeatmapActivityTypesUseCase
86103
) {
87104
self.fetchUserDataUseCase = fetchUserDataUseCase
105+
self.fetchProfileImageDataUseCase = fetchProfileImageDataUseCase
88106
self.fetchTodosUseCase = fetchTodosUseCase
89107
self.upsertStatusMessageUseCase = upsertStatusMessageUseCase
90108
self.networkConnectivityUseCase = networkConnectivityUseCase
@@ -121,14 +139,25 @@ final class ProfileViewModel: StorePattern {
121139
case .tapResetStatusMessageButton:
122140
state.statusMessage = ""
123141
case .fetchUserData(let profile):
142+
let previousAvatarURL = state.avatarURL
124143
state.name = profile.name
125144
state.email = profile.email
126145
state.statusMessage = profile.statusMessage
127146
state.avatarURL = profile.avatarURL
147+
if previousAvatarURL != profile.avatarURL {
148+
state.avatarImageData = nil
149+
}
150+
if let avatarURL = profile.avatarURL {
151+
effects = [.fetchAvatarImageData(avatarURL)]
152+
}
128153
if state.earliestQuarterStart == nil {
129154
state.earliestQuarterStart = quarterStart(for: profile.createdAt)
130155
?? calendar.startOfDay(for: profile.createdAt)
131156
}
157+
case .setAvatarImageData(let url, let data):
158+
guard state.avatarURL == url else { break }
159+
let id = (state.avatarImageData?.id ?? 0) + 1
160+
state.avatarImageData = ProfileAvatarImageData(id: id, data: data)
132161
case .setQuarterPickerPresented(let isPresented):
133162
state.showQuarterPicker = isPresented
134163
case .setQuarterPickerYear(let year):
@@ -202,6 +231,13 @@ final class ProfileViewModel: StorePattern {
202231
send(.setAlert(true))
203232
}
204233
}
234+
case .fetchAvatarImageData(let url):
235+
Task {
236+
do {
237+
let data = try await fetchProfileImageDataUseCase.execute(from: url)
238+
send(.setAvatarImageData(url, data))
239+
} catch { }
240+
}
205241
case .fetchActivityQuarter(let quarterStart):
206242
beginLoading(mode: .delayed)
207243
Task {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// ProfileViewModelTests.swift
3+
// DevLogPresentationTests
4+
//
5+
// Created by opfic on 6/11/26.
6+
//
7+
8+
import Testing
9+
import Foundation
10+
import DevLogDomain
11+
@testable import DevLogPresentation
12+
13+
@MainActor
14+
struct ProfileViewModelTests {
15+
@Test("같은 아바타 URL을 다시 받아도 프로필 이미지 데이터를 다시 요청한다")
16+
func 같은_아바타_URL을_다시_받아도_프로필_이미지_데이터를_다시_요청한다() async {
17+
let imageData = Data([1, 2, 3])
18+
let spy = FetchProfileImageDataUseCaseSpy(data: imageData)
19+
let viewModel = makeProfileViewModel(fetchProfileImageDataUseCase: spy)
20+
let avatarURL = URL(string: "https://example.com/avatar.png")!
21+
let profile = UserProfile(
22+
name: "opfic",
23+
email: "opfic@example.com",
24+
statusMessage: "status",
25+
avatarURL: avatarURL,
26+
createdAt: Date(timeIntervalSince1970: 0)
27+
)
28+
29+
viewModel.send(.fetchUserData(profile))
30+
await waitUntil {
31+
spy.calledURLs == [avatarURL]
32+
}
33+
34+
viewModel.send(.fetchUserData(profile))
35+
await waitUntil {
36+
spy.calledURLs == [avatarURL, avatarURL]
37+
}
38+
39+
#expect(spy.calledURLs == [avatarURL, avatarURL])
40+
#expect(viewModel.state.avatarImageData?.data == imageData)
41+
}
42+
}
43+
44+
@MainActor
45+
private func makeProfileViewModel(
46+
fetchProfileImageDataUseCase: FetchProfileImageDataUseCase
47+
) -> ProfileViewModel {
48+
ProfileViewModel(
49+
fetchUserDataUseCase: FetchUserDataUseCaseSpy(
50+
profile: UserProfile(
51+
name: "opfic",
52+
email: "opfic@example.com",
53+
statusMessage: "",
54+
avatarURL: nil,
55+
createdAt: Date(timeIntervalSince1970: 0)
56+
)
57+
),
58+
fetchProfileImageDataUseCase: fetchProfileImageDataUseCase,
59+
fetchTodosUseCase: FetchTodosUseCaseSpy(),
60+
upsertStatusMessageUseCase: UpsertStatusMessageUseCaseSpy(),
61+
networkConnectivityUseCase: ObserveNetworkConnectivityUseCaseSpy(),
62+
fetchHeatmapActivityTypesUseCase: FetchHeatmapActivityTypesUseCaseSpy(),
63+
updateHeatmapActivityTypesUseCase: UpdateHeatmapActivityTypesUseCaseSpy()
64+
)
65+
}

Application/DevLogPresentation/Tests/Support/TestSupport.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,56 @@ final class FetchTodosUseCaseSpy: FetchTodosUseCase {
192192
}
193193
}
194194

195+
final class FetchUserDataUseCaseSpy: FetchUserDataUseCase {
196+
var profile: UserProfile
197+
198+
init(profile: UserProfile) {
199+
self.profile = profile
200+
}
201+
202+
func execute() async throws -> UserProfile {
203+
profile
204+
}
205+
}
206+
207+
final class FetchProfileImageDataUseCaseSpy: FetchProfileImageDataUseCase {
208+
var data: Data
209+
private(set) var calledURLs: [URL] = []
210+
211+
init(data: Data) {
212+
self.data = data
213+
}
214+
215+
func execute(from url: URL) async throws -> Data {
216+
calledURLs.append(url)
217+
return data
218+
}
219+
}
220+
221+
final class UpsertStatusMessageUseCaseSpy: UpsertStatusMessageUseCase {
222+
private(set) var messages: [String] = []
223+
224+
func execute(_ message: String) async throws {
225+
messages.append(message)
226+
}
227+
}
228+
229+
final class FetchHeatmapActivityTypesUseCaseSpy: FetchHeatmapActivityTypesUseCase {
230+
var activityTypes: [String] = []
231+
232+
func execute() -> [String] {
233+
activityTypes
234+
}
235+
}
236+
237+
final class UpdateHeatmapActivityTypesUseCaseSpy: UpdateHeatmapActivityTypesUseCase {
238+
private(set) var activityTypes: [[String]] = []
239+
240+
func execute(_ activityTypes: [String]) {
241+
self.activityTypes.append(activityTypes)
242+
}
243+
}
244+
195245
final class FetchWebPagesUseCaseSpy: FetchWebPagesUseCase {
196246
var webPages: [WebPage]
197247
private(set) var calledQueries: [String] = []

0 commit comments

Comments
 (0)