Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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()
.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