Skip to content

Commit 21cf83b

Browse files
authored
[#223] 웹페이지 썸네일에 대한 용량 처리를 개선한다 (#224)
* feat: 웹페이지 컨텐츠를 제거할 때, 로컬에 저장된 이미지도 제거 * feat: 재 fetch 시 기존 데이터를 덮어씌우도록 구현 * feat: 사용하지 않는 이미지 파일에 대해 백그라운드 스레드에서 제거하는 로직 구현 * fix: 웹페이지 썸네일 캐시 복구 로직 수정 및 삭제 시 캐시 정리 * refactor: 보안 상 Firestore에 저장된 로컬 URL을 바로 신뢰하지 않고 일련의 검사를 하도록 개선
1 parent 7b31be3 commit 21cf83b

3 files changed

Lines changed: 113 additions & 23 deletions

File tree

DevLog/Data/Repository/WebPageRepositoryImpl.swift

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import UIKit
910

1011
final class WebPageRepositoryImpl: WebPageRepository {
1112
private let webPageService: WebPageService
@@ -25,8 +26,12 @@ final class WebPageRepositoryImpl: WebPageRepository {
2526
pages.reserveCapacity(responses.count)
2627

2728
for response in responses {
28-
if needsImageRestore(response), let restored = try? await restoreWebPage(response) {
29-
pages.append(restored)
29+
if await needsImageRestore(response) {
30+
if let restored = try? await restoreWebPage(response) {
31+
pages.append(restored)
32+
} else if let page = try? responseWithoutImage(response).toDomain() {
33+
pages.append(page)
34+
}
3035
continue
3136
}
3237
if let page = try? response.toDomain() {
@@ -50,17 +55,40 @@ final class WebPageRepositoryImpl: WebPageRepository {
5055

5156
func delete(_ urlString: String) async throws {
5257
try await webPageService.deleteWebPage(urlString)
58+
await metadataService.removeCachedImage(for: urlString)
5359
}
5460
}
5561

5662
private extension WebPageRepositoryImpl {
57-
func needsImageRestore(_ response: WebPageResponse) -> Bool {
63+
func needsImageRestore(_ response: WebPageResponse) async -> Bool {
5864
guard !response.imageURL.isEmpty,
59-
let url = URL(string: response.imageURL),
60-
url.isFileURL else {
65+
let imageURL = URL(string: response.imageURL),
66+
imageURL.isFileURL else {
6167
return false
6268
}
63-
return !FileManager.default.fileExists(atPath: url.path)
69+
70+
let expectedImageURL: URL
71+
do {
72+
expectedImageURL = try metadataService.cachedImageURL(for: response.url)
73+
} catch {
74+
return true
75+
}
76+
77+
if imageURL.standardizedFileURL != expectedImageURL.standardizedFileURL {
78+
return true
79+
}
80+
81+
return await Task.detached(priority: .utility) {
82+
guard FileManager.default.fileExists(atPath: imageURL.path) else {
83+
return true
84+
}
85+
86+
guard let imageData = try? Data(contentsOf: imageURL) else {
87+
return true
88+
}
89+
90+
return UIImage(data: imageData) == nil
91+
}.value
6492
}
6593

6694
func restoreWebPage(_ response: WebPageResponse) async throws -> WebPage? {
@@ -83,4 +111,14 @@ private extension WebPageRepositoryImpl {
83111

84112
return try? newResponse.toDomain()
85113
}
114+
115+
func responseWithoutImage(_ response: WebPageResponse) -> WebPageResponse {
116+
WebPageResponse(
117+
id: response.id,
118+
title: response.title,
119+
url: response.url,
120+
displayURL: response.displayURL,
121+
imageURL: ""
122+
)
123+
}
86124
}

DevLog/Infra/Service/WebPageMetadataService.swift

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import UIKit
1111

1212
final class WebPageMetadataService {
1313
private let logger = Logger(category: "WebPageMetadataService")
14+
1415
func fetchMetadata(from urlString: String) async throws -> WebPageMetadataResponse {
1516
logger.info("Fetching metadata for URL: \(urlString)")
1617

@@ -38,6 +39,36 @@ final class WebPageMetadataService {
3839
}
3940
}
4041

42+
func removeCachedImage(for urlString: String) async {
43+
guard let url = URL(string: urlString) else {
44+
logger.error("Invalid URL for cached image removal: \(urlString)")
45+
return
46+
}
47+
48+
do {
49+
let removed = try await Task.detached(priority: .utility) {
50+
let fileURL = try Self.cacheFileURL(for: url)
51+
guard FileManager.default.fileExists(atPath: fileURL.path) else { return false }
52+
try FileManager.default.removeItem(at: fileURL)
53+
return true
54+
}.value
55+
56+
if removed {
57+
logger.info("Removed cached image for URL: \(urlString)")
58+
}
59+
} catch {
60+
logger.error("Failed to remove cached image", error: error)
61+
}
62+
}
63+
64+
func cachedImageURL(for urlString: String) throws -> URL {
65+
guard let url = URL(string: urlString) else {
66+
throw URLError(.badURL)
67+
}
68+
69+
return try Self.cacheFileURL(for: url)
70+
}
71+
4172
private func extractImageURL(from imageProvider: NSItemProvider?, url: URL) async throws -> URL? {
4273
guard let imageProvider else { return nil }
4374

@@ -58,13 +89,6 @@ final class WebPageMetadataService {
5889
let fileURL = try Self.cacheFileURL(for: url)
5990
Task.detached { [data, fileURL] in
6091
do {
61-
if FileManager.default.fileExists(atPath: fileURL.path) {
62-
if let existingData = try? Data(contentsOf: fileURL),
63-
UIImage(data: existingData) != nil {
64-
continuation.resume(returning: fileURL)
65-
return
66-
}
67-
}
6892
try data.write(to: fileURL, options: [.atomic])
6993
continuation.resume(returning: fileURL)
7094
} catch {
@@ -79,6 +103,17 @@ final class WebPageMetadataService {
79103
}
80104

81105
private static func cacheFileURL(for url: URL) throws -> URL {
106+
let imageDir = try imageDirectoryURL()
107+
108+
let fileName = url.absoluteString
109+
.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? UUID().uuidString
110+
111+
return imageDir
112+
.appendingPathComponent(fileName)
113+
.appendingPathExtension("jpeg")
114+
}
115+
116+
private static func imageDirectoryURL() throws -> URL {
82117
let cachesDir = try FileManager.default.url(
83118
for: .cachesDirectory,
84119
in: .userDomainMask,
@@ -90,11 +125,6 @@ final class WebPageMetadataService {
90125
try FileManager.default.createDirectory(at: imageDir, withIntermediateDirectories: true)
91126
}
92127

93-
let fileName = url.absoluteString
94-
.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? UUID().uuidString
95-
96128
return imageDir
97-
.appendingPathComponent(fileName)
98-
.appendingPathExtension("jpeg")
99129
}
100130
}

DevLog/UI/Common/Component/WebItemRow.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ struct WebItemRow: View {
1515

1616
var body: some View {
1717
HStack {
18-
CacheableImage(url: item.imageURL) {
19-
Image(systemName: "globe")
20-
.resizable()
21-
.scaledToFit()
22-
}
18+
thumbnail
2319
.frame(width: sceneWidth / 10, height: sceneWidth / 10)
2420
.clipShape(RoundedRectangle(cornerRadius: 10))
2521

@@ -41,4 +37,30 @@ struct WebItemRow: View {
4137
}
4238
.padding(.vertical, 4)
4339
}
40+
41+
@ViewBuilder
42+
private var thumbnail: some View {
43+
if let imageURL = item.imageURL {
44+
AsyncImage(url: imageURL) { phase in
45+
switch phase {
46+
case .success(let image):
47+
image
48+
.resizable()
49+
.scaledToFill()
50+
case .empty:
51+
ProgressView()
52+
default:
53+
placeholderImage
54+
}
55+
}
56+
} else {
57+
placeholderImage
58+
}
59+
}
60+
61+
private var placeholderImage: some View {
62+
Image(systemName: "globe")
63+
.resizable()
64+
.scaledToFit()
65+
}
4466
}

0 commit comments

Comments
 (0)