Skip to content

Commit 5c3a4f6

Browse files
authored
[#79] 웹페이지 데이터를 디바운싱을 적용한 키워드 입력에 따라 불러오는 로직을 추가한다 (#99)
* feat: 검색 쿼리에 대해 디바운스 400ms 적용 * refactor: 메서드 파라미터를 배열 -> OrderedSet으로 변경 * ui: 새 컨텐츠 작성 시 웹페이지를 추가할 수 있도록 UI 개선 * feat: 웹페이지 데이터를 url에서 각종 메타 데이터까지 확대 및 저장 및 불러오기 수정 * style: 액션 명 변경 * feat: 얼럿을 통해 웹페이지를 저장할 수 있도록 추가 * refactor: 얼럿 로직 정리 * ui: 타이틀 좌측 정렬 및 최대 2줄까지 허용 * ui: 터치 영역 최대화 * fix: tmp URL 대신 캐시 디렉토리에 저장되도록 변경하여 이미지가 날라가는 현상 해결
1 parent f9aa6ea commit 5c3a4f6

17 files changed

Lines changed: 400 additions & 191 deletions

DevLog/Data/Mapper/WebPageMapping.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,23 @@
55
// Created by 최윤진 on 2/19/26.
66
//
77

8+
import Foundation
9+
810
extension WebPageResponse {
9-
func toDomain() -> WebPage {
10-
WebPage(
11+
func toDomain() throws -> WebPage {
12+
guard let url = URL(string: url) else {
13+
throw DataError.invalidData("WebPageResponse.url is invalid: \(url)")
14+
}
15+
guard let displayURL = URL(string: displayURL) else {
16+
throw DataError.invalidData("WebPageResponse.displayURL is invalid: \(displayURL)")
17+
}
18+
let imageURL: URL?
19+
if !self.imageURL.isEmpty {
20+
imageURL = URL(string: self.imageURL)
21+
} else {
22+
imageURL = nil
23+
}
24+
return WebPage(
1125
title: title,
1226
url: url,
1327
displayURL: displayURL,

DevLog/Data/Repository/WebPageRepositoryImpl.swift

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,28 @@ final class WebPageRepositoryImpl: WebPageRepository {
1717
self.metadataService = metadataService
1818
}
1919

20-
func fetch() async throws -> [WebPage] {
21-
let responses = try await webPageService.fetchWebPages()
22-
let indexedResponses = responses.enumerated().map { ($0.offset, $0.element) }
23-
24-
return try await withThrowingTaskGroup(of: (Int, WebPageResponse?).self) { group in
25-
for (index, response) in indexedResponses {
26-
group.addTask {
27-
let metadata = try? await self.metadataService.fetchMetadata(from: response)
28-
return (index, metadata)
29-
}
30-
}
31-
32-
var results: [WebPageResponse?] = Array(repeating: nil, count: responses.count)
33-
for try await (index, metadata) in group {
34-
results[index] = metadata
35-
}
36-
37-
return results.compactMap { $0?.toDomain() }
38-
}
20+
func fetch(_ query: String) async throws -> [WebPage] {
21+
try await webPageService
22+
.fetchWebPages(query)
23+
.compactMap { try? $0.toDomain() }
3924
}
4025

4126
func upsert(_ urlString: String) async throws -> WebPage {
42-
try await webPageService.upsertWebPage(urlString)
43-
let response = WebPageURLResponse(urlString: urlString)
44-
let metadata = try await metadataService.fetchMetadata(from: response)
45-
return metadata.toDomain()
27+
let metadata = try await metadataService.fetchMetadata(from: urlString)
28+
let request = WebPageRequest(
29+
title: metadata.title,
30+
url: urlString,
31+
displayURL: metadata.displayURL,
32+
imageURL: metadata.imageURL
33+
)
34+
try await webPageService.upsertWebPage(request)
35+
let response = WebPageResponse(
36+
title: request.title,
37+
url: request.url,
38+
displayURL: request.displayURL,
39+
imageURL: request.imageURL
40+
)
41+
return try response.toDomain()
4642
}
4743

4844
func delete(_ urlString: String) async throws {

DevLog/Domain/Protocol/WebPageRepository.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//
77

88
protocol WebPageRepository {
9-
func fetch() async throws -> [WebPage]
9+
func fetch(_ query: String) async throws -> [WebPage]
1010
func upsert(_ urlString: String) async throws -> WebPage
1111
func delete(_ urlString: String) async throws
1212
}

DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPagesUseCase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
//
77

88
protocol FetchWebPagesUseCase {
9-
func execute() async throws -> [WebPage]
9+
func execute(_ query: String) async throws -> [WebPage]
1010
}

DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPagesUseCaseImpl.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ final class FetchWebPagesUseCaseImpl: FetchWebPagesUseCase {
1212
self.repository = repository
1313
}
1414

15-
func execute() async throws -> [WebPage] {
16-
try await repository.fetch()
15+
func execute(_ query: String) async throws -> [WebPage] {
16+
try await repository.fetch(query)
1717
}
1818
}

DevLog/Infra/DTO/WebPageDTO.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// WebPageResponse.swift
3+
// DevLog
4+
//
5+
// Created by 최윤진 on 2/9/26.
6+
//
7+
8+
import FirebaseFirestore
9+
10+
struct WebPageRequest: Encodable {
11+
let title: String
12+
let url: String
13+
let displayURL: String
14+
let imageURL: String
15+
}
16+
17+
struct WebPageResponse: Decodable {
18+
@DocumentID var id: String?
19+
let title: String
20+
let url: String
21+
let displayURL: String
22+
let imageURL: String
23+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// WebPageMetadataResponse.swift
3+
// DevLog
4+
//
5+
// Created by 최윤진 on 2/20/26.
6+
//
7+
8+
struct WebPageMetadataResponse {
9+
let title: String
10+
let displayURL: String
11+
let imageURL: String
12+
}

DevLog/Infra/DTO/WebPageResponse.swift

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

DevLog/Infra/DTO/WebPageURLResponse.swift

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

DevLog/Infra/Service/WebPageMetadataService.swift

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77

88
import Foundation
99
import LinkPresentation
10+
import UIKit
1011

1112
final class WebPageMetadataService {
1213
private let logger = Logger(category: "WebPageMetadataService")
13-
func fetchMetadata(from response: WebPageURLResponse) async throws -> WebPageResponse {
14-
logger.info("Fetching metadata for URL: \(response.urlString)")
14+
func fetchMetadata(from urlString: String) async throws -> WebPageMetadataResponse {
15+
logger.info("Fetching metadata for URL: \(urlString)")
1516

16-
guard let url = URL(string: response.urlString) else {
17-
logger.error("Invalid URL: \(response.urlString)")
17+
guard let url = URL(string: urlString) else {
18+
logger.error("Invalid URL: \(urlString)")
1819
throw URLError(.badURL)
1920
}
2021

@@ -26,11 +27,10 @@ final class WebPageMetadataService {
2627
let imageURL = try await extractImageURL(from: metadata.imageProvider, url: url)
2728

2829
logger.info("Successfully fetched metadata for: \(metadata.title ?? "Unknown")")
29-
return WebPageResponse(
30-
title: metadata.title,
31-
url: url,
32-
displayURL: metadata.url ?? url,
33-
imageURL: imageURL
30+
return WebPageMetadataResponse(
31+
title: metadata.title ?? "",
32+
displayURL: (metadata.url ?? url).absoluteString,
33+
imageURL: imageURL?.absoluteString ?? ""
3434
)
3535
} catch {
3636
logger.error("Failed to fetch metadata", error: error)
@@ -43,30 +43,58 @@ final class WebPageMetadataService {
4343

4444
return try await withCheckedThrowingContinuation { continuation in
4545
imageProvider.loadObject(ofClass: UIImage.self) { image, error in
46-
guard let image = image as? UIImage,
47-
let data = image.jpegData(compressionQuality: 1.0) else {
48-
continuation.resume(returning: nil)
46+
if let error {
47+
continuation.resume(throwing: error)
4948
return
5049
}
5150

52-
guard let fileName = url.absoluteString.addingPercentEncoding(
53-
withAllowedCharacters: .alphanumerics
54-
) else {
51+
guard let image = image as? UIImage,
52+
let data = image.jpegData(compressionQuality: 1.0) else {
5553
continuation.resume(returning: nil)
5654
return
5755
}
5856

59-
let tempURL = FileManager.default.temporaryDirectory
60-
.appendingPathComponent(fileName)
61-
.appendingPathExtension("jpeg")
62-
6357
do {
64-
try data.write(to: tempURL)
65-
continuation.resume(returning: tempURL)
58+
let fileURL = try Self.cacheFileURL(for: url)
59+
Task.detached { [data, fileURL] in
60+
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+
}
68+
try data.write(to: fileURL, options: [.atomic])
69+
continuation.resume(returning: fileURL)
70+
} catch {
71+
continuation.resume(throwing: error)
72+
}
73+
}
6674
} catch {
6775
continuation.resume(throwing: error)
6876
}
6977
}
7078
}
7179
}
80+
81+
private static func cacheFileURL(for url: URL) throws -> URL {
82+
let cachesDir = try FileManager.default.url(
83+
for: .cachesDirectory,
84+
in: .userDomainMask,
85+
appropriateFor: nil,
86+
create: true
87+
)
88+
let imageDir = cachesDir.appendingPathComponent("webPageImages", isDirectory: true)
89+
if !FileManager.default.fileExists(atPath: imageDir.path) {
90+
try FileManager.default.createDirectory(at: imageDir, withIntermediateDirectories: true)
91+
}
92+
93+
let fileName = url.absoluteString
94+
.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? UUID().uuidString
95+
96+
return imageDir
97+
.appendingPathComponent(fileName)
98+
.appendingPathExtension("jpeg")
99+
}
72100
}

0 commit comments

Comments
 (0)