Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2250441
refactor: 유저 uid 의존성 추가
opficdev Feb 8, 2026
4199a84
refactor: 필요한 데이터만을 받도록 개선
opficdev Feb 8, 2026
9a1a036
refactor: 순수 도메인 모델 재구성
opficdev Feb 8, 2026
1e3449b
refactor: 순수하게 받아오는 형태로 변경
opficdev Feb 8, 2026
a68ce48
fix: 데이터 저장 필드명 통일
opficdev Feb 9, 2026
152ab5f
feat: 각 레이어에 필요한 DTO / 모델 구현
opficdev Feb 9, 2026
a399153
feat: 웹페이지의 메타 데이터를 받아오는 서비스 객체 구현
opficdev Feb 9, 2026
46de911
feat: 웹페이지의 메타 데이터를 받아오는 데이터 레이어 구현
opficdev Feb 9, 2026
6a287fd
feat: 웹페이지 데이터를 추가, 삭제 및 불러오는 도메인 레이어 구현
opficdev Feb 9, 2026
1431ca3
feat: SearchViewModel 구현 및 적용
opficdev Feb 9, 2026
a5683d6
fix: 비동기 호출에 따른 append로 뷰에 표시될 때 순서가 달라지는 현상 해결
opficdev Feb 9, 2026
dc30e6c
fix: LoadingView 중일 때 하단에 컨텐츠가 비치는 현상 제거
opficdev Feb 9, 2026
9a00f9c
refactor: url의 host 및 url에 포함된 특수문자 처리
opficdev Feb 9, 2026
9fae0ae
design: 주변 패딩 제거
opficdev Feb 9, 2026
26314f9
ui: padding 추가
opficdev Feb 9, 2026
380e68a
feat: webInfoCard에 내비게이션 추가
opficdev Feb 9, 2026
1907d9c
refactor: 저장 원 url 그대로 불러오도록 개선
opficdev Feb 9, 2026
9972a83
fix: 스와이프 액션으로 웹페이지 리스트 요소가 제거되지 않는 현상 수정
opficdev Feb 9, 2026
dcf2091
feat: 이미지를 로드하지 못했을 때의 이미지를 밖에서 받아올 수 있도록 추가
opficdev Feb 9, 2026
6c22eca
feat: WebPageView의 내비게이션 타이틀 설정
opficdev Feb 9, 2026
24be9b2
feat: 로컬 파일 URL도 가능하도록 구현
opficdev Feb 9, 2026
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 DevLog/App/Assembler/DataAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,12 @@ final class DataAssembler: Assembler {
container.register(PushNotificationRepository.self) {
PushNotificationRepositoryImpl(pushNotificationService: container.resolve(PushNotificationService.self))
}

container.register(WebPageRepository.self) {
WebPageRepositoryImpl(
webPageService: container.resolve(WebPageService.self),
metadataService: container.resolve(WebPageMetadataService.self)
)
}
}
}
12 changes: 12 additions & 0 deletions DevLog/App/Assembler/DomainAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,17 @@ final class DomainAssembler: Assembler {
container.register(FetchTodosByKindUseCase.self) {
FetchTodosByKindUseCaseImpl(container.resolve(TodoRepository.self))
}

container.register(FetchWebPagesUseCase.self) {
FetchWebPagesUseCaseImpl(container.resolve(WebPageRepository.self))
}

container.register(AddWebPageUseCase.self) {
AddWebPageUseCaseImpl(container.resolve(WebPageRepository.self))
}

container.register(DeleteWebPageUseCase.self) {
DeleteWebPageUseCaseImpl(container.resolve(WebPageRepository.self))
}
}
}
8 changes: 8 additions & 0 deletions DevLog/App/Assembler/InfraAssembler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,13 @@ final class InfraAssembler: Assembler {
container.register(PushNotificationService.self) {
PushNotificationService()
}

container.register(WebPageService.self) {
WebPageService()
}

container.register(WebPageMetadataService.self) {
WebPageMetadataService()
}
}
}
24 changes: 24 additions & 0 deletions DevLog/Data/DTO/WebPageMetadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// WebPageMetadata.swift
// DevLog
//
// Created by 최윤진 on 2/9/26.
//

import Foundation

struct WebPageMetadata: Hashable {
let title: String?
let url: URL
let displayURL: URL
let imageURL: URL?

func toDomain() -> WebPage {
WebPage(
title: title,
url: url,
displayURL: displayURL,
imageURL: imageURL
)
}
}
50 changes: 50 additions & 0 deletions DevLog/Data/Repository/WebPageRepositoryImpl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// WebPageRepositoryImpl.swift
// DevLog
//
// Created by 최윤진 on 2/8/26.
//

final class WebPageRepositoryImpl: WebPageRepository {
private let webPageService: WebPageService
private let metadataService: WebPageMetadataService

init(
webPageService: WebPageService,
metadataService: WebPageMetadataService
) {
self.webPageService = webPageService
self.metadataService = metadataService
}

func fetch() async throws -> [WebPageMetadata] {
let responses = try await webPageService.fetchWebPages()
let indexedResponses = responses.enumerated().map { ($0.offset, $0.element) }

return try await withThrowingTaskGroup(of: (Int, WebPageMetadata?).self) { group in
for (index, response) in indexedResponses {
group.addTask {
let metadata = try? await self.metadataService.fetchMetadata(from: response)
return (index, metadata)
}
}

var results: [WebPageMetadata?] = Array(repeating: nil, count: responses.count)
for try await (index, metadata) in group {
results[index] = metadata
}

return results.compactMap { $0 }
}
}

func upsert(_ urlString: String) async throws -> WebPageMetadata {
try await webPageService.upsertWebPage(urlString)
let response = WebPageResponse(urlString: urlString)
return try await metadataService.fetchMetadata(from: response)
}

func delete(_ urlString: String) async throws {
try await webPageService.deleteWebPage(urlString)
}
}
15 changes: 15 additions & 0 deletions DevLog/Domain/Entity/WebPage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// WebPage.swift
// DevLog
//
// Created by 최윤진 on 2/9/26.
//

import Foundation

struct WebPage {
let title: String?
let url: URL
let displayURL: URL
let imageURL: URL?
}
12 changes: 12 additions & 0 deletions DevLog/Domain/Protocol/WebPageRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// WebPageRepository.swift
// DevLog
//
// Created by 최윤진 on 2/8/26.
//

protocol WebPageRepository {
func fetch() async throws -> [WebPageMetadata]
func upsert(_ urlString: String) async throws -> WebPageMetadata
func delete(_ urlString: String) async throws
}
11 changes: 11 additions & 0 deletions DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPagesUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// FetchWebPagesUseCase.swift
// DevLog
//
// Created by 최윤진 on 2/9/26.
//

protocol FetchWebPagesUseCase {
var repository: WebPageRepository { get }
func execute() async throws -> [WebPage]
}
18 changes: 18 additions & 0 deletions DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPagesUseCaseImpl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// FetchWebUseCaseImpl.swift
// DevLog
//
// Created by 최윤진 on 2/9/26.
//

final class FetchWebPagesUseCaseImpl: FetchWebPagesUseCase {
let repository: WebPageRepository

init(_ repository: WebPageRepository) {
self.repository = repository
}

func execute() async throws -> [WebPage] {
return try await repository.fetch().map { $0.toDomain() }
}
}
11 changes: 11 additions & 0 deletions DevLog/Domain/UseCase/WebPage/Upsert/AddWebPageUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// AddWebPageUseCase.swift
// DevLog
//
// Created by 최윤진 on 2/8/26.
//

protocol AddWebPageUseCase {
var repository: WebPageRepository { get }
func execute(_ urlString: String) async throws -> WebPage
}
18 changes: 18 additions & 0 deletions DevLog/Domain/UseCase/WebPage/Upsert/AddWebPageUseCaseImpl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// AddWebPageUseCaseImpl.swift
// DevLog
//
// Created by 최윤진 on 2/9/26.
//

final class AddWebPageUseCaseImpl: AddWebPageUseCase {
let repository: WebPageRepository

init(_ repository: WebPageRepository) {
self.repository = repository
}

func execute(_ urlString: String) async throws -> WebPage {
try await repository.upsert(urlString).toDomain()
}
}
11 changes: 11 additions & 0 deletions DevLog/Domain/UseCase/WebPage/Upsert/DeleteWebPageUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// DeleteWebPageUseCase.swift
// DevLog
//
// Created by 최윤진 on 2/9/26.
//

protocol DeleteWebPageUseCase {
var repository: WebPageRepository { get }
func execute(_ urlString: String) async throws
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// DeleteWebPageUseCaseImpl.swift
// DevLog
//
// Created by 최윤진 on 2/9/26.
//

final class DeleteWebPageUseCaseImpl: DeleteWebPageUseCase {
var repository: WebPageRepository

init(_ repository: WebPageRepository) {
self.repository = repository
}

func execute(_ urlString: String) async throws {
try await repository.delete(urlString)
}
}

76 changes: 0 additions & 76 deletions DevLog/Infra/DTO/WebPageInfo.swift

This file was deleted.

10 changes: 10 additions & 0 deletions DevLog/Infra/DTO/WebPageResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// WebPageResponse.swift
// DevLog
//
// Created by 최윤진 on 2/9/26.
//

struct WebPageResponse {
let urlString: String
}
63 changes: 63 additions & 0 deletions DevLog/Infra/Service/WebPageMetadataService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// WebPageMetadataService.swift
// DevLog
//
// Created by 최윤진 on 2/9/26.
//

import Foundation
import LinkPresentation

final class WebPageMetadataService {
func fetchMetadata(from response: WebPageResponse) async throws -> WebPageMetadata {
guard let url = URL(string: response.urlString) else {
throw URLError(.badURL)
}

let provider = LPMetadataProvider()
provider.timeout = 10.0

let metadata = try await provider.startFetchingMetadata(for: url)

let imageURL = try await extractImageURL(from: metadata.imageProvider, url: url)

return WebPageMetadata(
title: metadata.title,
url: url,
displayURL: metadata.url ?? url,
imageURL: imageURL
)
}

private func extractImageURL(from imageProvider: NSItemProvider?, url: URL) async throws -> URL? {
guard let imageProvider else { return nil }

return try await withCheckedThrowingContinuation { continuation in
imageProvider.loadObject(ofClass: UIImage.self) { image, error in
guard let image = image as? UIImage,
let data = image.jpegData(compressionQuality: 1.0) else {
continuation.resume(returning: nil)
return
}

guard let fileName = url.absoluteString.addingPercentEncoding(
withAllowedCharacters: .alphanumerics
) else {
continuation.resume(returning: nil)
return
}

let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent(fileName)
.appendingPathExtension("jpeg")

do {
try data.write(to: tempURL)
continuation.resume(returning: tempURL)
} catch {
continuation.resume(throwing: error)
}
}
}
}
}
Loading