Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions Application/DevLogData/Sources/DataAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ public final class DataAssembler: Assembler {
UserDataRepositoryImpl(userService: container.resolve(UserService.self))
}

container.register(ProfileImageDataRepository.self) {
ProfileImageDataRepositoryImpl(
service: container.resolve(ProfileImageDataService.self),
store: container.resolve(MemoryCacheStore.self)
)
}

container.register(AnalyticsRepository.self) {
AnalyticsRepositoryImpl(
analyticsService: container.resolve(AnalyticsService.self)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// ProfileImageDataService.swift
// DevLogData
//
// Created by opfic on 6/11/26.
//

import Foundation

public protocol ProfileImageDataService {
func fetchImageData(from url: URL) async throws -> Data
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// ProfileImageDataRepositoryImpl.swift
// DevLogData
//
// Created by opfic on 6/11/26.
//

import Foundation
import DevLogDomain

public final class ProfileImageDataRepositoryImpl: ProfileImageDataRepository {
private let service: ProfileImageDataService
private let store: MemoryCacheStore

public init(
service: ProfileImageDataService,
store: MemoryCacheStore
) {
self.service = service
self.store = store
}

public func fetchImageData(from url: URL) async throws -> Data {
do {
let data = try await service.fetchImageData(from: url)
store.setValue(data, forKey: Self.cacheKey(for: url))
return data
} catch {
if let data: Data = store.value(forKey: Self.cacheKey(for: url)) {
return data
}
throw error
}
}

private static func cacheKey(for url: URL) -> String {
"profileImageData:\(url.absoluteString)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// ProfileImageDataRepositoryImplTests.swift
// DevLogDataTests
//
// Created by opfic on 6/11/26.
//

import Foundation
import Testing
@testable import DevLogData

struct ProfileImageDataRepositoryImplTests {
@Test("캐시가 있어도 원격 이미지 데이터를 다시 요청하고 성공 데이터를 저장한다")
func 캐시가_있어도_원격_이미지_데이터를_다시_요청하고_성공_데이터를_저장한다() async throws {
let cachedData = Data([1, 2, 3])
let remoteData = Data([4, 5, 6])
let service = ProfileImageDataServiceSpy(data: cachedData)
let store = ProfileImageMemoryCacheStoreSpy()
let repository = ProfileImageDataRepositoryImpl(service: service, store: store)
let url = URL(string: "https://example.com/avatar.png")!

_ = try await repository.fetchImageData(from: url)
service.data = remoteData
let data = try await repository.fetchImageData(from: url)

#expect(data == remoteData)
#expect(service.calledURLs == [url, url])
#expect(store.storedData == remoteData)
}

@Test("원격 이미지 요청 실패 시 메모리 캐시 데이터를 반환한다")
func 원격_이미지_요청_실패_시_메모리_캐시_데이터를_반환한다() async throws {
let cachedData = Data([1, 2, 3])
let service = ProfileImageDataServiceSpy(data: cachedData)
let store = ProfileImageMemoryCacheStoreSpy()
let repository = ProfileImageDataRepositoryImpl(service: service, store: store)
let url = URL(string: "https://example.com/avatar.png")!

_ = try await repository.fetchImageData(from: url)
service.error = ProfileImageDataRepositoryImplTestsError.serviceFailed
let data = try await repository.fetchImageData(from: url)

#expect(data == cachedData)
#expect(service.calledURLs == [url, url])
}
}

private final class ProfileImageDataServiceSpy: ProfileImageDataService {
var data: Data
var error: Error?
private(set) var calledURLs: [URL] = []

init(data: Data) {
self.data = data
}

func fetchImageData(from url: URL) async throws -> Data {
calledURLs.append(url)

if let error {
throw error
}

return data
}
}

private final class ProfileImageMemoryCacheStoreSpy: MemoryCacheStore {
private var values = [String: Any]()
private(set) var storedData: Data?

func value<T: Codable>(forKey key: String) -> T? {
values[key] as? T
}

func setValue<T: Codable>(_ value: T?, forKey key: String) {
guard let value else {
values.removeValue(forKey: key)
return
}

values[key] = value
storedData = value as? Data
}
}

private enum ProfileImageDataRepositoryImplTestsError: Error {
case serviceFailed
}
4 changes: 4 additions & 0 deletions Application/DevLogDomain/Sources/DomainAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ private extension DomainAssembler {
FetchUserDataUseCaseImpl(container.resolve(UserDataRepository.self))
}

container.register(FetchProfileImageDataUseCase.self) {
FetchProfileImageDataUseCaseImpl(container.resolve(ProfileImageDataRepository.self))
}

container.register(UpsertStatusMessageUseCase.self) {
UpsertStatusMessageUseCaseImpl(container.resolve(UserDataRepository.self))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// ProfileImageDataRepository.swift
// DevLogDomain
//
// Created by opfic on 6/11/26.
//

import Foundation

public protocol ProfileImageDataRepository {
func fetchImageData(from url: URL) async throws -> Data
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// FetchProfileImageDataUseCase.swift
// DevLogDomain
//
// Created by opfic on 6/11/26.
//

import Foundation

public protocol FetchProfileImageDataUseCase {
func execute(from url: URL) async throws -> Data
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// FetchProfileImageDataUseCaseImpl.swift
// DevLogDomain
//
// Created by opfic on 6/11/26.
//

import Foundation

public final class FetchProfileImageDataUseCaseImpl: FetchProfileImageDataUseCase {
private let repository: ProfileImageDataRepository

public init(_ repository: ProfileImageDataRepository) {
self.repository = repository
}

public func execute(from url: URL) async throws -> Data {
try await repository.fetchImageData(from: url)
}
}
4 changes: 4 additions & 0 deletions Application/DevLogInfra/Sources/InfraAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ public final class InfraAssembler: Assembler {
UserServiceImpl()
}

container.register(ProfileImageDataService.self) {
ProfileImageDataServiceImpl()
}

container.register(PushNotificationService.self) {
PushNotificationServiceImpl()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// ProfileImageDataServiceImpl.swift
// DevLogInfra
//
// Created by opfic on 6/11/26.
//

import Foundation
import Nexa
import DevLogData

final class ProfileImageDataServiceImpl: ProfileImageDataService {
func fetchImageData(from url: URL) async throws -> Data {
try await NXAPIClient(
configuration: NXClientConfiguration(baseURL: url)
)
.get(url.absoluteString)
.timeout(10)
.intercept(ProfileImageDataCachePolicyInterceptor())
.validate(.successStatusCode)
.raw()
.data
}
Comment thread
opficdev marked this conversation as resolved.
}

private struct ProfileImageDataCachePolicyInterceptor: NXHTTPInterceptor {
func intercept(
context: NXRequestExecutionContext,
next: @escaping @Sendable (NXRequestExecutionContext) async throws -> NXRawResponse
) async throws -> NXRawResponse {
var request = context.request
request.cachePolicy = .reloadIgnoringLocalCacheData
return try await next(context.replacingRequest(request))
}
}

This file was deleted.

Loading