From 7a690019f016f7f697ddfef6686c194dde4ffd6d Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 11 Sep 2026 18:18:52 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=9B=B9=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EC=99=84=EC=A0=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppGraph+PresentationDependencies.swift | 9 - .../App/Sources/App/Graph/AppGraph.swift | 21 +- .../App/Sources/App/Graph/InfraGraphSet.swift | 11 - .../App/Graph/PersistenceGraphSet.swift | 1 - .../Sources/App/Graph/WebPageGraphSet.swift | 49 ---- Application/Data/Sources/DTO/WebPageDTO.swift | 53 ----- .../Sources/DTO/WebPageMetadataResponse.swift | 22 -- .../Graph/WebPageImageRepositoryGraph.swift | 33 --- .../Graph/WebPageRepositoryGraph.swift | 37 --- .../Data/Sources/Mapper/WebPageMapping.swift | 33 --- .../Sources/Protocol/AnalyticsService.swift | 1 - .../Sources/Protocol/WebPageImageStore.swift | 16 -- .../Protocol/WebPageMetadataService.swift | 14 -- .../Sources/Protocol/WebPageService.swift | 15 -- .../Repository/AnalyticsRepositoryImpl.swift | 2 - .../WebPageImageRepositoryImpl.swift | 29 --- .../Repository/WebPageRepositoryImpl.swift | 153 ------------ .../WebPageRepositoryImplTests.swift | 225 ------------------ .../Sources/Entity/AnalyticsEvent.swift | 1 - .../Domain/Sources/Entity/WebPage.swift | 30 --- .../Graph/WebPageImageUseCaseGraph.swift | 29 --- .../Sources/Graph/WebPageUseCaseGraph.swift | 39 --- .../Protocol/WebPageImageRepository.swift | 11 - .../Sources/Protocol/WebPageRepository.swift | 13 - .../FetchWebPageImageDirSizeUseCase.swift | 10 - .../FetchWebPageImageDirSizeUseCaseImpl.swift | 18 -- .../WebPage/Fetch/FetchWebPagesUseCase.swift | 10 - .../Fetch/FetchWebPagesUseCaseImpl.swift | 18 -- .../WebPage/Upsert/AddWebPageUseCase.swift | 10 - .../Upsert/AddWebPageUseCaseImpl.swift | 18 -- .../ClearWebPageImageDirectoryUseCase.swift | 10 - ...learWebPageImageDirectoryUseCaseImpl.swift | 18 -- .../WebPage/Upsert/DeleteWebPageUseCase.swift | 10 - .../Upsert/DeleteWebPageUseCaseImpl.swift | 18 -- .../Upsert/UndoDeleteWebPageUseCase.swift | 10 - .../Upsert/UndoDeleteWebPageUseCaseImpl.swift | 18 -- .../Infra/Sources/Common/FirestorePath.swift | 8 - .../Graph/WebPageMetadataServiceGraph.swift | 25 -- .../Sources/Graph/WebPageServiceGraph.swift | 19 -- .../FirebaseAnalyticsServiceImpl.swift | 5 - .../Sources/Service/FunctionAPIEndpoint.swift | 10 - .../Service/WebPageMetadataServiceImpl.swift | 135 ----------- .../Sources/Service/WebPageServiceImpl.swift | 177 -------------- .../Graph/WebPageImageStoreGraph.swift | 19 -- .../Persistence/WebPageImageStoreImpl.swift | 194 --------------- .../WebPageImageStoreImplTests.swift | 52 ---- .../Resources/Localizable.xcstrings | 51 ---- .../ProfileDependencyPreparation.swift | 9 - .../Sources/Settings/SettingsFeature.swift | 85 ------- .../Sources/Settings/SettingsView.swift | 38 --- .../Settings/SettingsFeatureTestDoubles.swift | 27 --- .../Tests/Settings/SettingsFeatureTests.swift | 105 +------- README.md | 14 +- 53 files changed, 8 insertions(+), 1980 deletions(-) delete mode 100644 Application/App/Sources/App/Graph/WebPageGraphSet.swift delete mode 100644 Application/Data/Sources/DTO/WebPageDTO.swift delete mode 100644 Application/Data/Sources/DTO/WebPageMetadataResponse.swift delete mode 100644 Application/Data/Sources/Graph/WebPageImageRepositoryGraph.swift delete mode 100644 Application/Data/Sources/Graph/WebPageRepositoryGraph.swift delete mode 100644 Application/Data/Sources/Mapper/WebPageMapping.swift delete mode 100644 Application/Data/Sources/Protocol/WebPageImageStore.swift delete mode 100644 Application/Data/Sources/Protocol/WebPageMetadataService.swift delete mode 100644 Application/Data/Sources/Protocol/WebPageService.swift delete mode 100644 Application/Data/Sources/Repository/WebPageImageRepositoryImpl.swift delete mode 100644 Application/Data/Sources/Repository/WebPageRepositoryImpl.swift delete mode 100644 Application/Data/Tests/Repository/WebPageRepositoryImplTests.swift delete mode 100644 Application/Domain/Sources/Entity/WebPage.swift delete mode 100644 Application/Domain/Sources/Graph/WebPageImageUseCaseGraph.swift delete mode 100644 Application/Domain/Sources/Graph/WebPageUseCaseGraph.swift delete mode 100644 Application/Domain/Sources/Protocol/WebPageImageRepository.swift delete mode 100644 Application/Domain/Sources/Protocol/WebPageRepository.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPagesUseCase.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPagesUseCaseImpl.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Upsert/AddWebPageUseCase.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Upsert/AddWebPageUseCaseImpl.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCase.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCaseImpl.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCase.swift delete mode 100644 Application/Domain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCaseImpl.swift delete mode 100644 Application/Infra/Sources/Graph/WebPageMetadataServiceGraph.swift delete mode 100644 Application/Infra/Sources/Graph/WebPageServiceGraph.swift delete mode 100644 Application/Infra/Sources/Service/WebPageMetadataServiceImpl.swift delete mode 100644 Application/Infra/Sources/Service/WebPageServiceImpl.swift delete mode 100644 Application/Persistence/Sources/Graph/WebPageImageStoreGraph.swift delete mode 100644 Application/Persistence/Sources/Persistence/WebPageImageStoreImpl.swift delete mode 100644 Application/Persistence/Tests/Persistence/WebPageImageStoreImplTests.swift diff --git a/Application/App/Sources/App/Dependency/AppGraph+PresentationDependencies.swift b/Application/App/Sources/App/Dependency/AppGraph+PresentationDependencies.swift index c967ec44..8b3d7e7f 100644 --- a/Application/App/Sources/App/Dependency/AppGraph+PresentationDependencies.swift +++ b/Application/App/Sources/App/Dependency/AppGraph+PresentationDependencies.swift @@ -192,15 +192,6 @@ private extension AppGraph { .userPreferencesUseCaseGraph .updateSystemThemeUseCase ) - ProfilePresentationDependencyPreparation.prepareSettingsStorage( - &dependencies, - fetchWebPageImageDirSizeUseCase: webPageGraphSet - .webPageImageUseCaseGraph - .fetchWebPageImageDirSizeUseCase, - clearWebPageImageDirectoryUseCase: webPageGraphSet - .webPageImageUseCaseGraph - .clearWebPageImageDirectoryUseCase - ) ProfilePresentationDependencyPreparation.prepareAccount( &dependencies, fetchAuthProvidersUseCase: authenticationGraphSet diff --git a/Application/App/Sources/App/Graph/AppGraph.swift b/Application/App/Sources/App/Graph/AppGraph.swift index ee42be17..95306ba5 100644 --- a/Application/App/Sources/App/Graph/AppGraph.swift +++ b/Application/App/Sources/App/Graph/AppGraph.swift @@ -16,12 +16,8 @@ final class AppGraph { } @Provide - private func makeInfraGraphSet( - persistenceGraphSet: PersistenceGraphSet - ) -> InfraGraphSet { - InfraGraphSet( - webPageImageStore: persistenceGraphSet.webPageImageStoreGraph.webPageImageStore - ) + private func makeInfraGraphSet() -> InfraGraphSet { + InfraGraphSet() } @Provide @@ -75,19 +71,6 @@ final class AppGraph { ) } - @Provide - private func makeWebPageGraphSet( - persistenceGraphSet: PersistenceGraphSet, - infraGraphSet: InfraGraphSet - ) -> WebPageGraphSet { - WebPageGraphSet( - authServiceGraph: infraGraphSet.authServiceGraph, - webPageMetadataServiceGraph: infraGraphSet.webPageMetadataServiceGraph, - webPageServiceGraph: infraGraphSet.webPageServiceGraph, - webPageImageStoreGraph: persistenceGraphSet.webPageImageStoreGraph - ) - } - @Provide private func makeUserProfileGraphSet( persistenceGraphSet: PersistenceGraphSet, diff --git a/Application/App/Sources/App/Graph/InfraGraphSet.swift b/Application/App/Sources/App/Graph/InfraGraphSet.swift index 8ac2a034..7bfdba4e 100644 --- a/Application/App/Sources/App/Graph/InfraGraphSet.swift +++ b/Application/App/Sources/App/Graph/InfraGraphSet.swift @@ -5,7 +5,6 @@ // Created by opfic on 9/7/26. // -import Data import Infra final class InfraGraphSet { @@ -29,15 +28,5 @@ final class InfraGraphSet { let userServiceGraph = UserServiceGraph() let profileImageDataServiceGraph = ProfileImageDataServiceGraph() let pushNotificationServiceGraph = PushNotificationServiceGraph() - let webPageServiceGraph = WebPageServiceGraph() - let webPageMetadataServiceGraph: WebPageMetadataServiceGraph let networkConnectivityProviderGraph = NWPathConnectivityProviderGraph() - - init(webPageImageStore: WebPageImageStore) { - self.webPageMetadataServiceGraph = WebPageMetadataServiceGraph( - input: WebPageMetadataServiceGraphInput( - webPageImageStore: webPageImageStore - ) - ) - } } diff --git a/Application/App/Sources/App/Graph/PersistenceGraphSet.swift b/Application/App/Sources/App/Graph/PersistenceGraphSet.swift index 435000a6..bed5a5a0 100644 --- a/Application/App/Sources/App/Graph/PersistenceGraphSet.swift +++ b/Application/App/Sources/App/Graph/PersistenceGraphSet.swift @@ -11,5 +11,4 @@ final class PersistenceGraphSet { let userDefaultsStoreGraph = UserDefaultsStoreGraph() let memoryCacheStoreGraph = MemoryCacheStoreGraph() let themeStoreGraph = ThemeStoreGraph() - let webPageImageStoreGraph = WebPageImageStoreGraph() } diff --git a/Application/App/Sources/App/Graph/WebPageGraphSet.swift b/Application/App/Sources/App/Graph/WebPageGraphSet.swift deleted file mode 100644 index 915f3c87..00000000 --- a/Application/App/Sources/App/Graph/WebPageGraphSet.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// WebPageGraphSet.swift -// App -// -// Created by opfic on 9/7/26. -// - -import Data -import Domain -import Infra -import Persistence - -final class WebPageGraphSet { - let webPageRepositoryGraph: WebPageRepositoryGraph - let webPageImageRepositoryGraph: WebPageImageRepositoryGraph - let webPageUseCaseGraph: WebPageUseCaseGraph - let webPageImageUseCaseGraph: WebPageImageUseCaseGraph - - init( - authServiceGraph: AuthServiceGraph, - webPageMetadataServiceGraph: WebPageMetadataServiceGraph, - webPageServiceGraph: WebPageServiceGraph, - webPageImageStoreGraph: WebPageImageStoreGraph - ) { - self.webPageRepositoryGraph = WebPageRepositoryGraph( - input: WebPageRepositoryGraphInput( - authService: authServiceGraph.authService, - metadataService: webPageMetadataServiceGraph.webPageMetadataService, - webPageService: webPageServiceGraph.webPageService - ) - ) - self.webPageImageRepositoryGraph = WebPageImageRepositoryGraph( - input: WebPageImageRepositoryGraphInput( - authService: authServiceGraph.authService, - store: webPageImageStoreGraph.webPageImageStore - ) - ) - self.webPageUseCaseGraph = WebPageUseCaseGraph( - input: WebPageUseCaseGraphInput( - repository: webPageRepositoryGraph.webPageRepository - ) - ) - self.webPageImageUseCaseGraph = WebPageImageUseCaseGraph( - input: WebPageImageUseCaseGraphInput( - repository: webPageImageRepositoryGraph.webPageImageRepository - ) - ) - } -} diff --git a/Application/Data/Sources/DTO/WebPageDTO.swift b/Application/Data/Sources/DTO/WebPageDTO.swift deleted file mode 100644 index 6deaeb01..00000000 --- a/Application/Data/Sources/DTO/WebPageDTO.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// WebPageDTO.swift -// Data -// -// Created by 최윤진 on 2/9/26. -// - -import Foundation -import Domain - -public struct WebPageRequest: Encodable { - public let title: String - public let url: String - public let displayURL: String - public let imageURL: String - public let isDeleted: Bool - - public init( - title: String, - url: String, - displayURL: String, - imageURL: String, - isDeleted: Bool - ) { - self.title = title - self.url = url - self.displayURL = displayURL - self.imageURL = imageURL - self.isDeleted = isDeleted - } -} - -public struct WebPageResponse { - public let id: String - public let title: String - public let url: String - public let displayURL: String - public let imageURL: String - - public init( - id: String, - title: String, - url: String, - displayURL: String, - imageURL: String - ) { - self.id = id - self.title = title - self.url = url - self.displayURL = displayURL - self.imageURL = imageURL - } -} diff --git a/Application/Data/Sources/DTO/WebPageMetadataResponse.swift b/Application/Data/Sources/DTO/WebPageMetadataResponse.swift deleted file mode 100644 index 6d0678c2..00000000 --- a/Application/Data/Sources/DTO/WebPageMetadataResponse.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// WebPageMetadataResponse.swift -// Data -// -// Created by 최윤진 on 2/20/26. -// - -public struct WebPageMetadataResponse { - public let title: String - public let displayURL: String - public let imageURL: String - - public init( - title: String, - displayURL: String, - imageURL: String - ) { - self.title = title - self.displayURL = displayURL - self.imageURL = imageURL - } -} diff --git a/Application/Data/Sources/Graph/WebPageImageRepositoryGraph.swift b/Application/Data/Sources/Graph/WebPageImageRepositoryGraph.swift deleted file mode 100644 index 016ada06..00000000 --- a/Application/Data/Sources/Graph/WebPageImageRepositoryGraph.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// WebPageImageRepositoryGraph.swift -// Data -// -// Created by opfic on 9/7/26. -// - -import Cradle -import Domain - -public struct WebPageImageRepositoryGraphInput { - public let authService: AuthService - public let store: WebPageImageStore - - public init( - authService: AuthService, - store: WebPageImageStore - ) { - self.authService = authService - self.store = store - } -} - -@DependencyGraph(input: WebPageImageRepositoryGraphInput.self) -public final class WebPageImageRepositoryGraph { - @Provide - private func makeWebPageImageRepository() -> WebPageImageRepository { - WebPageImageRepositoryImpl( - authService: input.authService, - store: input.store - ) - } -} diff --git a/Application/Data/Sources/Graph/WebPageRepositoryGraph.swift b/Application/Data/Sources/Graph/WebPageRepositoryGraph.swift deleted file mode 100644 index 13b599fb..00000000 --- a/Application/Data/Sources/Graph/WebPageRepositoryGraph.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// WebPageRepositoryGraph.swift -// Data -// -// Created by opfic on 9/7/26. -// - -import Cradle -import Domain - -public struct WebPageRepositoryGraphInput { - public let authService: AuthService - public let metadataService: WebPageMetadataService - public let webPageService: WebPageService - - public init( - authService: AuthService, - metadataService: WebPageMetadataService, - webPageService: WebPageService - ) { - self.authService = authService - self.metadataService = metadataService - self.webPageService = webPageService - } -} - -@DependencyGraph(input: WebPageRepositoryGraphInput.self) -public final class WebPageRepositoryGraph { - @Provide - private func makeWebPageRepository() -> WebPageRepository { - WebPageRepositoryImpl( - authService: input.authService, - metadataService: input.metadataService, - webPageService: input.webPageService - ) - } -} diff --git a/Application/Data/Sources/Mapper/WebPageMapping.swift b/Application/Data/Sources/Mapper/WebPageMapping.swift deleted file mode 100644 index 8f67087d..00000000 --- a/Application/Data/Sources/Mapper/WebPageMapping.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// WebPageMapping.swift -// Data -// -// Created by 최윤진 on 2/19/26. -// - -import Foundation -import Domain - -public extension WebPageResponse { - func toDomain() throws -> WebPage { - guard let url = URL(string: url) else { - throw DataLayerError.invalidData("WebPageResponse.url is invalid: \(url)") - } - guard let displayURL = URL(string: displayURL) else { - throw DataLayerError.invalidData("WebPageResponse.displayURL is invalid: \(displayURL)") - } - let imageURL: URL? - if !self.imageURL.isEmpty { - imageURL = URL(string: self.imageURL) - } else { - imageURL = nil - } - return WebPage( - id: id, - title: title, - url: url, - displayURL: displayURL, - imageURL: imageURL - ) - } -} diff --git a/Application/Data/Sources/Protocol/AnalyticsService.swift b/Application/Data/Sources/Protocol/AnalyticsService.swift index b95960b6..e77cff32 100644 --- a/Application/Data/Sources/Protocol/AnalyticsService.swift +++ b/Application/Data/Sources/Protocol/AnalyticsService.swift @@ -9,6 +9,5 @@ public protocol AnalyticsService { func trackScreenView(_ name: String) func trackTodoCreate() func trackTodoComplete() - func trackWebPageCreate() func trackPushOpen() } diff --git a/Application/Data/Sources/Protocol/WebPageImageStore.swift b/Application/Data/Sources/Protocol/WebPageImageStore.swift deleted file mode 100644 index 682c1c1a..00000000 --- a/Application/Data/Sources/Protocol/WebPageImageStore.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// WebPageImageStore.swift -// Data -// -// Created by opfic on 5/14/26. -// - -import Foundation - -public protocol WebPageImageStore { - func cachedImageURL(for url: URL, accountID: String?) async throws -> URL - func saveImage(_ data: Data, for url: URL, accountID: String?) async throws -> URL - func dirSizeInBytes(accountID: String?) async -> Int64 - func clearDirectory(accountID: String?) async throws - func removeImage(for url: URL, accountID: String?) async throws -> Bool -} diff --git a/Application/Data/Sources/Protocol/WebPageMetadataService.swift b/Application/Data/Sources/Protocol/WebPageMetadataService.swift deleted file mode 100644 index 7a20ab36..00000000 --- a/Application/Data/Sources/Protocol/WebPageMetadataService.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// WebPageMetadataService.swift -// Data -// -// Created by opfic on 5/14/26. -// - -import Foundation - -public protocol WebPageMetadataService { - func fetchMetadata(from urlString: String, accountID: String?) async throws -> WebPageMetadataResponse - func removeCachedImage(for urlString: String, accountID: String?) async - func cachedImageURL(for urlString: String, accountID: String?) async throws -> URL -} diff --git a/Application/Data/Sources/Protocol/WebPageService.swift b/Application/Data/Sources/Protocol/WebPageService.swift deleted file mode 100644 index e0838f00..00000000 --- a/Application/Data/Sources/Protocol/WebPageService.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// WebPageService.swift -// Data -// -// Created by opfic on 5/14/26. -// - -import Foundation - -public protocol WebPageService { - func fetchWebPages(_ query: String) async throws -> [WebPageResponse] - func upsertWebPage(_ request: WebPageRequest) async throws - func deleteWebPage(_ id: String) async throws - func undoDeleteWebPage(_ id: String) async throws -} diff --git a/Application/Data/Sources/Repository/AnalyticsRepositoryImpl.swift b/Application/Data/Sources/Repository/AnalyticsRepositoryImpl.swift index b9998ad4..e363784d 100644 --- a/Application/Data/Sources/Repository/AnalyticsRepositoryImpl.swift +++ b/Application/Data/Sources/Repository/AnalyticsRepositoryImpl.swift @@ -22,8 +22,6 @@ final class AnalyticsRepositoryImpl: AnalyticsRepository { analyticsService.trackTodoCreate() case .todoComplete: analyticsService.trackTodoComplete() - case .webPageCreate: - analyticsService.trackWebPageCreate() case .pushOpen: analyticsService.trackPushOpen() } diff --git a/Application/Data/Sources/Repository/WebPageImageRepositoryImpl.swift b/Application/Data/Sources/Repository/WebPageImageRepositoryImpl.swift deleted file mode 100644 index 9517e7ad..00000000 --- a/Application/Data/Sources/Repository/WebPageImageRepositoryImpl.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// WebPageImageRepositoryImpl.swift -// Data -// -// Created by opfic on 4/14/26. -// - -import Domain - -final class WebPageImageRepositoryImpl: WebPageImageRepository { - private let authService: AuthService - private let store: WebPageImageStore - - init( - authService: AuthService, - store: WebPageImageStore - ) { - self.authService = authService - self.store = store - } - - func fetchDirSizeInBytes() async -> Int64 { - await store.dirSizeInBytes(accountID: authService.uid) - } - - func clearDirectory() async throws { - try await store.clearDirectory(accountID: authService.uid) - } -} diff --git a/Application/Data/Sources/Repository/WebPageRepositoryImpl.swift b/Application/Data/Sources/Repository/WebPageRepositoryImpl.swift deleted file mode 100644 index 5926f2b7..00000000 --- a/Application/Data/Sources/Repository/WebPageRepositoryImpl.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// WebPageRepositoryImpl.swift -// Data -// -// Created by 최윤진 on 2/8/26. -// - -import Foundation -import UIKit -import Domain - -final class WebPageRepositoryImpl: WebPageRepository { - private let authService: AuthService - private let metadataService: WebPageMetadataService - private let webPageService: WebPageService - - init( - authService: AuthService, - metadataService: WebPageMetadataService, - webPageService: WebPageService - ) { - self.authService = authService - self.metadataService = metadataService - self.webPageService = webPageService - } - - func fetch(_ query: String) async throws -> [WebPage] { - do { - let accountID = authService.uid - let responses = try await webPageService.fetchWebPages(query) - var pages: [WebPage] = [] - pages.reserveCapacity(responses.count) - - for response in responses { - if await needsImageRestore(response, accountID: accountID) { - if let restored = try? await restoreWebPage(response, accountID: accountID) { - pages.append(restored) - } else if let page = try? responseWithoutImage(response).toDomain() { - pages.append(page) - } - continue - } - if let page = try? response.toDomain() { - pages.append(page) - } - } - - return pages - } catch { - throw error.toDomain() - } - } - - func upsert(_ urlString: String) async throws { - do { - let accountID = authService.uid - let metadata = try await metadataService.fetchMetadata(from: urlString, accountID: accountID) - let request = WebPageRequest( - title: metadata.title, - url: urlString, - displayURL: metadata.displayURL, - imageURL: metadata.imageURL, - isDeleted: false - ) - try await webPageService.upsertWebPage(request) - } catch { - throw error.toDomain() - } - } - - func delete(id: String, urlString: String) async throws { - do { - let accountID = authService.uid - try await webPageService.deleteWebPage(id) - await metadataService.removeCachedImage(for: urlString, accountID: accountID) - } catch { - throw error.toDomain() - } - } - - func undoDelete(_ id: String) async throws { - do { - try await webPageService.undoDeleteWebPage(id) - } catch { - throw error.toDomain() - } - } -} - -private extension WebPageRepositoryImpl { - func needsImageRestore(_ response: WebPageResponse, accountID: String?) async -> Bool { - guard !response.imageURL.isEmpty, - let imageURL = URL(string: response.imageURL), - imageURL.isFileURL else { - return false - } - - let expectedImageURL: URL - do { - expectedImageURL = try await metadataService.cachedImageURL(for: response.url, accountID: accountID) - } catch { - return true - } - - if imageURL.standardizedFileURL != expectedImageURL.standardizedFileURL { - return true - } - - return await Task.detached(priority: .utility) { - guard FileManager.default.fileExists(atPath: imageURL.path) else { - return true - } - - guard let imageData = try? Data(contentsOf: imageURL) else { - return true - } - - return UIImage(data: imageData) == nil - }.value - } - - func restoreWebPage(_ response: WebPageResponse, accountID: String?) async throws -> WebPage? { - let metadata = try await metadataService.fetchMetadata(from: response.url, accountID: accountID) - let request = WebPageRequest( - title: metadata.title, - url: response.url, - displayURL: metadata.displayURL, - imageURL: metadata.imageURL, - isDeleted: false - ) - try await webPageService.upsertWebPage(request) - - let newResponse = WebPageResponse( - id: response.id, - title: metadata.title, - url: response.url, - displayURL: metadata.displayURL, - imageURL: metadata.imageURL - ) - - return try? newResponse.toDomain() - } - - func responseWithoutImage(_ response: WebPageResponse) -> WebPageResponse { - WebPageResponse( - id: response.id, - title: response.title, - url: response.url, - displayURL: response.displayURL, - imageURL: "" - ) - } -} diff --git a/Application/Data/Tests/Repository/WebPageRepositoryImplTests.swift b/Application/Data/Tests/Repository/WebPageRepositoryImplTests.swift deleted file mode 100644 index efb8869f..00000000 --- a/Application/Data/Tests/Repository/WebPageRepositoryImplTests.swift +++ /dev/null @@ -1,225 +0,0 @@ -// -// WebPageRepositoryImplTests.swift -// DataTests -// -// Created by opfic on 7/8/26. -// - -import Combine -import Foundation -import Testing -import Domain -@testable import Data - -struct WebPageRepositoryImplTests { - @Test("웹페이지 저장은 현재 계정 scope로 메타데이터 캐시를 생성한다") - func 웹페이지_저장은_현재_계정_scope로_메타데이터_캐시를_생성한다() async throws { - let expectedImageURL = URL(fileURLWithPath: "/account-a/webPageImages/image.jpeg") - let fixture = makeFixture( - uid: "account-a", - expectedImageURL: expectedImageURL - ) - let urlString = "https://example.com/article" - - try await fixture.repository.upsert(urlString) - - #expect(await fixture.metadataService.fetchMetadataRequests() == [ - WebPageMetadataServiceRequest(urlString: urlString, accountID: "account-a") - ]) - let requests = await fixture.webPageService.upsertedRequests() - let request = try #require(requests.first) - #expect(requests.count == 1) - #expect(request.title == "metadata-title") - #expect(request.url == urlString) - #expect(request.displayURL == urlString) - #expect(request.imageURL == expectedImageURL.absoluteString) - #expect(request.isDeleted == false) - } - - @Test("웹페이지 삭제는 현재 계정 scope의 캐시만 제거한다") - func 웹페이지_삭제는_현재_계정_scope의_캐시만_제거한다() async throws { - let fixture = makeFixture(uid: "account-a") - let urlString = "https://example.com/article" - - try await fixture.repository.delete(id: "web-page-id", urlString: urlString) - - #expect(await fixture.webPageService.deletedIDs() == ["web-page-id"]) - #expect(await fixture.metadataService.removeCachedImageRequests() == [ - WebPageMetadataServiceRequest(urlString: urlString, accountID: "account-a") - ]) - } - - @Test("현재 계정 scope와 다른 파일 캐시는 재사용하지 않고 복구한다") - func 현재_계정_scope와_다른_파일_캐시는_재사용하지_않고_복구한다() async throws { - let expectedImageURL = URL(fileURLWithPath: "/account-a/webPageImages/image.jpeg") - let legacyImageURL = URL(fileURLWithPath: "/legacy/webPageImages/image.jpeg") - let urlString = "https://example.com/article" - let response = WebPageResponse( - id: "web-page-id", - title: "legacy-title", - url: urlString, - displayURL: urlString, - imageURL: legacyImageURL.absoluteString - ) - let fixture = makeFixture( - uid: "account-a", - responses: [response], - expectedImageURL: expectedImageURL - ) - - let pages = try await fixture.repository.fetch("") - - #expect(await fixture.metadataService.cachedImageURLRequests() == [ - WebPageMetadataServiceRequest(urlString: urlString, accountID: "account-a") - ]) - #expect(await fixture.metadataService.fetchMetadataRequests() == [ - WebPageMetadataServiceRequest(urlString: urlString, accountID: "account-a") - ]) - let requests = await fixture.webPageService.upsertedRequests() - let request = try #require(requests.first) - let page = try #require(pages.first) - #expect(requests.count == 1) - #expect(request.title == "metadata-title") - #expect(request.url == urlString) - #expect(request.displayURL == urlString) - #expect(request.imageURL == expectedImageURL.absoluteString) - #expect(page.imageURL == expectedImageURL) - } - - private func makeFixture( - uid: String?, - responses: [WebPageResponse] = [], - expectedImageURL: URL = URL(fileURLWithPath: "/account/webPageImages/image.jpeg") - ) -> WebPageRepositoryFixture { - let authService = WebPageRepositoryAuthServiceSpy(uid: uid) - let metadataService = WebPageMetadataServiceSpy( - response: WebPageMetadataResponse( - title: "metadata-title", - displayURL: "https://example.com/article", - imageURL: expectedImageURL.absoluteString - ), - expectedImageURL: expectedImageURL - ) - let webPageService = WebPageServiceSpy(responses: responses) - let repository = WebPageRepositoryImpl( - authService: authService, - metadataService: metadataService, - webPageService: webPageService - ) - - return WebPageRepositoryFixture( - metadataService: metadataService, - repository: repository, - webPageService: webPageService - ) - } -} - -private struct WebPageRepositoryFixture { - let metadataService: WebPageMetadataServiceSpy - let repository: WebPageRepositoryImpl - let webPageService: WebPageServiceSpy -} - -private struct WebPageMetadataServiceRequest: Equatable { - let urlString: String - let accountID: String? -} - -private final class WebPageRepositoryAuthServiceSpy: AuthService { - private let subject: CurrentValueSubject - - var uid: String? - var providerIDs: [String] { [] } - var providerCount: Int { 0 } - - init(uid: String?) { - self.uid = uid - self.subject = CurrentValueSubject(uid != nil) - } - - func observeSignedIn() -> AnyPublisher { - subject.eraseToAnyPublisher() - } - - func beginSignIn() { } - func completeSignIn() { } - func cancelSignIn() { } - func getProviderID() async throws -> String? { nil } - func deleteCurrentUser() async throws { } - func clearCurrentSession() async throws { } -} - -private actor WebPageMetadataServiceSpy: WebPageMetadataService { - private let expectedImageURL: URL - private let response: WebPageMetadataResponse - private var cachedImageURLEvents = [WebPageMetadataServiceRequest]() - private var fetchMetadataEvents = [WebPageMetadataServiceRequest]() - private var removeCachedImageEvents = [WebPageMetadataServiceRequest]() - - init( - response: WebPageMetadataResponse, - expectedImageURL: URL - ) { - self.expectedImageURL = expectedImageURL - self.response = response - } - - func fetchMetadata(from urlString: String, accountID: String?) async throws -> WebPageMetadataResponse { - fetchMetadataEvents.append(WebPageMetadataServiceRequest(urlString: urlString, accountID: accountID)) - return response - } - - func removeCachedImage(for urlString: String, accountID: String?) async { - removeCachedImageEvents.append(WebPageMetadataServiceRequest(urlString: urlString, accountID: accountID)) - } - - func cachedImageURL(for urlString: String, accountID: String?) async throws -> URL { - cachedImageURLEvents.append(WebPageMetadataServiceRequest(urlString: urlString, accountID: accountID)) - return expectedImageURL - } - - func cachedImageURLRequests() -> [WebPageMetadataServiceRequest] { - cachedImageURLEvents - } - - func fetchMetadataRequests() -> [WebPageMetadataServiceRequest] { - fetchMetadataEvents - } - - func removeCachedImageRequests() -> [WebPageMetadataServiceRequest] { - removeCachedImageEvents - } -} - -private actor WebPageServiceSpy: WebPageService { - private let responses: [WebPageResponse] - private var deleted = [String]() - private var upserted = [WebPageRequest]() - - init(responses: [WebPageResponse] = []) { - self.responses = responses - } - - func fetchWebPages(_ query: String) async throws -> [WebPageResponse] { - responses - } - - func upsertWebPage(_ request: WebPageRequest) async throws { - upserted.append(request) - } - - func deleteWebPage(_ id: String) async throws { - deleted.append(id) - } - - func undoDeleteWebPage(_ id: String) async throws { } - - func deletedIDs() -> [String] { - deleted - } - - func upsertedRequests() -> [WebPageRequest] { - upserted - } -} diff --git a/Application/Domain/Sources/Entity/AnalyticsEvent.swift b/Application/Domain/Sources/Entity/AnalyticsEvent.swift index 12560ec4..5b40e93f 100644 --- a/Application/Domain/Sources/Entity/AnalyticsEvent.swift +++ b/Application/Domain/Sources/Entity/AnalyticsEvent.swift @@ -9,6 +9,5 @@ public enum AnalyticsEvent { case screenView(String) case todoCreate case todoComplete - case webPageCreate case pushOpen } diff --git a/Application/Domain/Sources/Entity/WebPage.swift b/Application/Domain/Sources/Entity/WebPage.swift deleted file mode 100644 index 8c6c4864..00000000 --- a/Application/Domain/Sources/Entity/WebPage.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// WebPage.swift -// Domain -// -// Created by 최윤진 on 2/9/26. -// - -import Foundation - -public struct WebPage: Hashable { - public let id: String - public let title: String? - public let url: URL - public let displayURL: URL - public let imageURL: URL? - - public init( - id: String, - title: String?, - url: URL, - displayURL: URL, - imageURL: URL? - ) { - self.id = id - self.title = title - self.url = url - self.displayURL = displayURL - self.imageURL = imageURL - } -} diff --git a/Application/Domain/Sources/Graph/WebPageImageUseCaseGraph.swift b/Application/Domain/Sources/Graph/WebPageImageUseCaseGraph.swift deleted file mode 100644 index 8f21adc8..00000000 --- a/Application/Domain/Sources/Graph/WebPageImageUseCaseGraph.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// WebPageImageUseCaseGraph.swift -// Domain -// -// Created by opfic on 9/7/26. -// - -import Cradle - -public struct WebPageImageUseCaseGraphInput { - public let repository: WebPageImageRepository - - public init(repository: WebPageImageRepository) { - self.repository = repository - } -} - -@DependencyGraph(input: WebPageImageUseCaseGraphInput.self) -public final class WebPageImageUseCaseGraph { - @Provide - private func makeFetchWebPageImageDirSizeUseCase() -> FetchWebPageImageDirSizeUseCase { - FetchWebPageImageDirSizeUseCaseImpl(input.repository) - } - - @Provide - private func makeClearWebPageImageDirectoryUseCase() -> ClearWebPageImageDirectoryUseCase { - ClearWebPageImageDirectoryUseCaseImpl(input.repository) - } -} diff --git a/Application/Domain/Sources/Graph/WebPageUseCaseGraph.swift b/Application/Domain/Sources/Graph/WebPageUseCaseGraph.swift deleted file mode 100644 index ac3ef267..00000000 --- a/Application/Domain/Sources/Graph/WebPageUseCaseGraph.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// WebPageUseCaseGraph.swift -// Domain -// -// Created by opfic on 9/7/26. -// - -import Cradle - -public struct WebPageUseCaseGraphInput { - public let repository: WebPageRepository - - public init(repository: WebPageRepository) { - self.repository = repository - } -} - -@DependencyGraph(input: WebPageUseCaseGraphInput.self) -public final class WebPageUseCaseGraph { - @Provide - private func makeFetchWebPagesUseCase() -> FetchWebPagesUseCase { - FetchWebPagesUseCaseImpl(input.repository) - } - - @Provide - private func makeAddWebPageUseCase() -> AddWebPageUseCase { - AddWebPageUseCaseImpl(input.repository) - } - - @Provide - private func makeDeleteWebPageUseCase() -> DeleteWebPageUseCase { - DeleteWebPageUseCaseImpl(input.repository) - } - - @Provide - private func makeUndoDeleteWebPageUseCase() -> UndoDeleteWebPageUseCase { - UndoDeleteWebPageUseCaseImpl(input.repository) - } -} diff --git a/Application/Domain/Sources/Protocol/WebPageImageRepository.swift b/Application/Domain/Sources/Protocol/WebPageImageRepository.swift deleted file mode 100644 index 7a6d8ab2..00000000 --- a/Application/Domain/Sources/Protocol/WebPageImageRepository.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// WebPageImageRepository.swift -// Domain -// -// Created by opfic on 4/14/26. -// - -public protocol WebPageImageRepository { - func fetchDirSizeInBytes() async -> Int64 - func clearDirectory() async throws -} diff --git a/Application/Domain/Sources/Protocol/WebPageRepository.swift b/Application/Domain/Sources/Protocol/WebPageRepository.swift deleted file mode 100644 index c2a6e9d6..00000000 --- a/Application/Domain/Sources/Protocol/WebPageRepository.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// WebPageRepository.swift -// Domain -// -// Created by 최윤진 on 2/8/26. -// - -public protocol WebPageRepository { - func fetch(_ query: String) async throws -> [WebPage] - func upsert(_ urlString: String) async throws - func delete(id: String, urlString: String) async throws - func undoDelete(_ id: String) async throws -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift b/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift deleted file mode 100644 index dae32203..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// FetchWebPageImageDirSizeUseCase.swift -// Domain -// -// Created by opfic on 4/14/26. -// - -public protocol FetchWebPageImageDirSizeUseCase { - func execute() async -> Int64 -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift b/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift deleted file mode 100644 index c19f478e..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FetchWebPageImageDirSizeUseCaseImpl.swift -// Domain -// -// Created by opfic on 4/14/26. -// - -public final class FetchWebPageImageDirSizeUseCaseImpl: FetchWebPageImageDirSizeUseCase { - private let repository: WebPageImageRepository - - init(_ repository: WebPageImageRepository) { - self.repository = repository - } - - public func execute() async -> Int64 { - await repository.fetchDirSizeInBytes() - } -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPagesUseCase.swift b/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPagesUseCase.swift deleted file mode 100644 index a7d8e672..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPagesUseCase.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// FetchWebPagesUseCase.swift -// Domain -// -// Created by 최윤진 on 2/9/26. -// - -public protocol FetchWebPagesUseCase { - func execute(_ query: String) async throws -> [WebPage] -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPagesUseCaseImpl.swift b/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPagesUseCaseImpl.swift deleted file mode 100644 index 80d837c9..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Fetch/FetchWebPagesUseCaseImpl.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FetchWebPagesUseCaseImpl.swift -// Domain -// -// Created by 최윤진 on 2/9/26. -// - -public final class FetchWebPagesUseCaseImpl: FetchWebPagesUseCase { - private let repository: WebPageRepository - - init(_ repository: WebPageRepository) { - self.repository = repository - } - - public func execute(_ query: String) async throws -> [WebPage] { - try await repository.fetch(query) - } -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Upsert/AddWebPageUseCase.swift b/Application/Domain/Sources/UseCase/WebPage/Upsert/AddWebPageUseCase.swift deleted file mode 100644 index 34d66e36..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Upsert/AddWebPageUseCase.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// AddWebPageUseCase.swift -// Domain -// -// Created by 최윤진 on 2/8/26. -// - -public protocol AddWebPageUseCase { - func execute(_ urlString: String) async throws -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Upsert/AddWebPageUseCaseImpl.swift b/Application/Domain/Sources/UseCase/WebPage/Upsert/AddWebPageUseCaseImpl.swift deleted file mode 100644 index bc27dbd9..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Upsert/AddWebPageUseCaseImpl.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// AddWebPageUseCaseImpl.swift -// Domain -// -// Created by 최윤진 on 2/9/26. -// - -public final class AddWebPageUseCaseImpl: AddWebPageUseCase { - private let repository: WebPageRepository - - init(_ repository: WebPageRepository) { - self.repository = repository - } - - public func execute(_ urlString: String) async throws { - try await repository.upsert(urlString) - } -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift b/Application/Domain/Sources/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift deleted file mode 100644 index 307f21a7..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// ClearWebPageImageDirectoryUseCase.swift -// Domain -// -// Created by opfic on 4/14/26. -// - -public protocol ClearWebPageImageDirectoryUseCase { - func execute() async throws -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift b/Application/Domain/Sources/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift deleted file mode 100644 index 885c8d4e..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ClearWebPageImageDirectoryUseCaseImpl.swift -// Domain -// -// Created by opfic on 4/14/26. -// - -public final class ClearWebPageImageDirectoryUseCaseImpl: ClearWebPageImageDirectoryUseCase { - private let repository: WebPageImageRepository - - init(_ repository: WebPageImageRepository) { - self.repository = repository - } - - public func execute() async throws { - try await repository.clearDirectory() - } -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCase.swift b/Application/Domain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCase.swift deleted file mode 100644 index cf789d10..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCase.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// DeleteWebPageUseCase.swift -// Domain -// -// Created by 최윤진 on 2/9/26. -// - -public protocol DeleteWebPageUseCase { - func execute(id: String, urlString: String) async throws -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCaseImpl.swift b/Application/Domain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCaseImpl.swift deleted file mode 100644 index bb887ecd..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Upsert/DeleteWebPageUseCaseImpl.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// DeleteWebPageUseCaseImpl.swift -// Domain -// -// Created by 최윤진 on 2/9/26. -// - -public final class DeleteWebPageUseCaseImpl: DeleteWebPageUseCase { - private let repository: WebPageRepository - - init(_ repository: WebPageRepository) { - self.repository = repository - } - - public func execute(id: String, urlString: String) async throws { - try await repository.delete(id: id, urlString: urlString) - } -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCase.swift b/Application/Domain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCase.swift deleted file mode 100644 index 715cc3d0..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCase.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// UndoDeleteWebPageUseCase.swift -// Domain -// -// Created by opfic on 3/16/26. -// - -public protocol UndoDeleteWebPageUseCase { - func execute(_ id: String) async throws -} diff --git a/Application/Domain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCaseImpl.swift b/Application/Domain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCaseImpl.swift deleted file mode 100644 index 455d33e9..00000000 --- a/Application/Domain/Sources/UseCase/WebPage/Upsert/UndoDeleteWebPageUseCaseImpl.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// UndoDeleteWebPageUseCaseImpl.swift -// Domain -// -// Created by opfic on 3/16/26. -// - -public final class UndoDeleteWebPageUseCaseImpl: UndoDeleteWebPageUseCase { - private let repository: WebPageRepository - - init(_ repository: WebPageRepository) { - self.repository = repository - } - - public func execute(_ id: String) async throws { - try await repository.undoDelete(id) - } -} diff --git a/Application/Infra/Sources/Common/FirestorePath.swift b/Application/Infra/Sources/Common/FirestorePath.swift index d39a458a..0a99e409 100644 --- a/Application/Infra/Sources/Common/FirestorePath.swift +++ b/Application/Infra/Sources/Common/FirestorePath.swift @@ -15,7 +15,6 @@ enum FirestorePath { case records case versions case notifications - case webPages } enum UserData: String { @@ -92,11 +91,4 @@ enum FirestorePath { "\(user(uid))/\(Collection.notifications.rawValue)" } - static func webPages(_ uid: String) -> String { - "\(user(uid))/\(Collection.webPages.rawValue)" - } - - static func webPage(_ uid: String, documentId: String) -> String { - "\(webPages(uid))/\(documentId)" - } } diff --git a/Application/Infra/Sources/Graph/WebPageMetadataServiceGraph.swift b/Application/Infra/Sources/Graph/WebPageMetadataServiceGraph.swift deleted file mode 100644 index eb7f008b..00000000 --- a/Application/Infra/Sources/Graph/WebPageMetadataServiceGraph.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// WebPageMetadataServiceGraph.swift -// Infra -// -// Created by opfic on 9/7/26. -// - -import Cradle -import Data - -public struct WebPageMetadataServiceGraphInput { - public let webPageImageStore: WebPageImageStore - - public init(webPageImageStore: WebPageImageStore) { - self.webPageImageStore = webPageImageStore - } -} - -@DependencyGraph(input: WebPageMetadataServiceGraphInput.self) -public final class WebPageMetadataServiceGraph { - @Provide - private func makeWebPageMetadataService() -> WebPageMetadataService { - WebPageMetadataServiceImpl(store: input.webPageImageStore) - } -} diff --git a/Application/Infra/Sources/Graph/WebPageServiceGraph.swift b/Application/Infra/Sources/Graph/WebPageServiceGraph.swift deleted file mode 100644 index 840e2ffb..00000000 --- a/Application/Infra/Sources/Graph/WebPageServiceGraph.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// WebPageServiceGraph.swift -// Infra -// -// Created by opfic on 9/7/26. -// - -import Cradle -import Data - -@DependencyGraph -public final class WebPageServiceGraph { - public init() { } - - @Provide - private func makeWebPageService() -> WebPageService { - WebPageServiceImpl() - } -} diff --git a/Application/Infra/Sources/Service/FirebaseAnalyticsServiceImpl.swift b/Application/Infra/Sources/Service/FirebaseAnalyticsServiceImpl.swift index b834b151..8d374949 100644 --- a/Application/Infra/Sources/Service/FirebaseAnalyticsServiceImpl.swift +++ b/Application/Infra/Sources/Service/FirebaseAnalyticsServiceImpl.swift @@ -12,7 +12,6 @@ final class FirebaseAnalyticsServiceImpl: AnalyticsService { private enum EventName { static let todoCreate = "todo_create" static let todoComplete = "todo_complete" - static let webPageCreate = "webpage_create" static let pushOpen = "push_open" } @@ -33,10 +32,6 @@ final class FirebaseAnalyticsServiceImpl: AnalyticsService { Analytics.logEvent(EventName.todoComplete, parameters: nil) } - func trackWebPageCreate() { - Analytics.logEvent(EventName.webPageCreate, parameters: nil) - } - func trackPushOpen() { Analytics.logEvent(EventName.pushOpen, parameters: nil) } diff --git a/Application/Infra/Sources/Service/FunctionAPIEndpoint.swift b/Application/Infra/Sources/Service/FunctionAPIEndpoint.swift index ec957c77..f7b96226 100644 --- a/Application/Infra/Sources/Service/FunctionAPIEndpoint.swift +++ b/Application/Infra/Sources/Service/FunctionAPIEndpoint.swift @@ -16,16 +16,6 @@ extension FunctionAPIEndpoint where Response == EmptyAPIResponse { Self(method: .delete, path: "/todos/\(functionAPIPathSegment(id))/deletion-request") } - // WebPage id는 이미 Firestore document id로 percent-encoded된 값 - // 여기서 다시 인코딩하면 Functions가 실제 문서 id와 다른 값을 받는다 - static func requestWebPageDeletion(_ id: String) -> Self { - Self(method: .post, path: "/web-pages/\(id)/deletion-request") - } - - static func undoWebPageDeletion(_ id: String) -> Self { - Self(method: .delete, path: "/web-pages/\(id)/deletion-request") - } - static func requestPushNotificationDeletion(_ id: String) -> Self { Self(method: .post, path: "/push-notifications/\(functionAPIPathSegment(id))/deletion-request") } diff --git a/Application/Infra/Sources/Service/WebPageMetadataServiceImpl.swift b/Application/Infra/Sources/Service/WebPageMetadataServiceImpl.swift deleted file mode 100644 index 36e68ba1..00000000 --- a/Application/Infra/Sources/Service/WebPageMetadataServiceImpl.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// WebPageMetadataServiceImpl.swift -// Infra -// -// Created by 최윤진 on 2/9/26. -// - -import Foundation -import LinkPresentation -import UIKit -import Core -import Data - -final class WebPageMetadataServiceImpl: WebPageMetadataService { - private enum CrashlyticsError { - static let domain = "DevLogInfra.WebPageMetadataServiceImpl" - - enum Code: Int { - case fetchMetadata = 1 - case removeCachedImage - case cachedImageURL - } - } - - private let imageStore: WebPageImageStore - private let logger = Logger(category: "WebPageMetadataServiceImpl") - - init(store: WebPageImageStore) { - self.imageStore = store - } - - func fetchMetadata(from urlString: String, accountID: String?) async throws -> WebPageMetadataResponse { - logger.info("Fetching metadata for URL: \(urlString)") - - guard let url = URL(string: urlString) else { - logger.error("Invalid URL: \(urlString)") - throw URLError(.badURL) - } - - do { - 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, accountID: accountID) - - logger.info("Successfully fetched metadata for: \(metadata.title ?? "Unknown")") - return WebPageMetadataResponse( - title: metadata.title ?? "", - displayURL: (metadata.url ?? url).absoluteString, - imageURL: imageURL?.absoluteString ?? "" - ) - } catch { - logger.error("Failed to fetch metadata", error: error) - record(error, code: .fetchMetadata) - throw error - } - } - - func removeCachedImage(for urlString: String, accountID: String?) async { - guard let url = URL(string: urlString) else { - logger.error("Invalid URL for cached image removal: \(urlString)") - return - } - - do { - let removed = try await imageStore.removeImage(for: url, accountID: accountID) - - if removed { - logger.info("Removed cached image for URL: \(urlString)") - } - } catch { - logger.error("Failed to remove cached image", error: error) - record(error, code: .removeCachedImage) - } - } - - func cachedImageURL(for urlString: String, accountID: String?) async throws -> URL { - guard let url = URL(string: urlString) else { - throw URLError(.badURL) - } - - do { - return try await imageStore.cachedImageURL(for: url, accountID: accountID) - } catch { - logger.error("Failed to fetch cached image URL", error: error) - record(error, code: .cachedImageURL) - throw error - } - } - - private func extractImageURL( - from imageProvider: NSItemProvider?, - url: URL, - accountID: String? - ) async throws -> URL? { - guard let imageProvider else { return nil } - - guard let data = try await imageData(from: imageProvider) else { return nil } - return try await imageStore.saveImage(data, for: url, accountID: accountID) - } - - private func imageData(from imageProvider: NSItemProvider) async throws -> Data? { - return try await withCheckedThrowingContinuation { continuation in - imageProvider.loadObject(ofClass: UIImage.self) { image, error in - if let error { - continuation.resume(throwing: error) - return - } - - guard let image = image as? UIImage, - let data = image.jpegData(compressionQuality: 1.0) else { - continuation.resume(returning: nil) - return - } - - continuation.resume(returning: data) - } - } - } -} - -private extension WebPageMetadataServiceImpl { - private static func record(_ error: Error, code: CrashlyticsError.Code) { - FirebaseCrashlyticsHelper.record( - error, - domain: "\(CrashlyticsError.domain).\(code)", - code: code.rawValue - ) - } - - private func record(_ error: Error, code: CrashlyticsError.Code) { - Self.record(error, code: code) - } -} diff --git a/Application/Infra/Sources/Service/WebPageServiceImpl.swift b/Application/Infra/Sources/Service/WebPageServiceImpl.swift deleted file mode 100644 index c7913f10..00000000 --- a/Application/Infra/Sources/Service/WebPageServiceImpl.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// WebPageServiceImpl.swift -// Infra -// -// Created by opfic on 6/3/25. -// - -import FirebaseAuth -import FirebaseFirestore -import Core -import Data - -final class WebPageServiceImpl: WebPageService { - private enum CrashlyticsError { - static let domain = "DevLogInfra.WebPageServiceImpl" - - enum Code: Int { - case fetchWebPages = 1 - case upsertWebPage - case deleteWebPage - case undoDeleteWebPage - } - } - - private let store = FirebaseConfiguration.firestore - private let encoder = Firestore.Encoder() - private let logger = Logger(category: "WebPageServiceImpl") - - /// 저장한 웹페이지를 모두 불러옴 - func fetchWebPages(_ query: String) async throws -> [WebPageResponse] { - logger.info("Fetching web pages") - - guard let uid = Auth.auth().currentUser?.uid else { - logger.error("User not authenticated") - throw DataLayerError.notAuthenticated - } - - do { - let collectionRef = store.collection(FirestorePath.webPages(uid)) - .whereField(WebPageFieldKey.isDeleted.rawValue, isEqualTo: false) - let snapshot = try await collectionRef.getDocuments() - let items: [WebPageResponse] = snapshot.documents.compactMap { makeResponse(from: $0) } - - let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmedQuery.isEmpty else { - logger.info("Successfully fetched \(items.count) web pages") - return items - } - - let filtered = items.filter { - $0.title.localizedCaseInsensitiveContains(trimmedQuery) || - $0.displayURL.localizedCaseInsensitiveContains(trimmedQuery) - } - logger.info("Successfully fetched \(filtered.count) web pages with query") - return filtered - } catch { - logger.error("Failed to fetch web pages", error: error) - record(error, code: .fetchWebPages) - throw error - } - } - - /// 웹페이지를 추가 또는 업데이트 - func upsertWebPage(_ request: WebPageRequest) async throws { - logger.info("Upserting web page: \(request.url)") - - guard let uid = Auth.auth().currentUser?.uid else { - logger.error("User not authenticated") - throw DataLayerError.notAuthenticated - } - - do { - let docID = documentID(for: request.url) - let docRef = store.document(FirestorePath.webPage(uid, documentId: docID)) - let data = try encoder.encode(request) - try await docRef.setData(data, merge: true) - logger.info("Successfully upserted web page") - } catch { - logger.error("Failed to upsert web page", error: error) - record(error, code: .upsertWebPage) - throw error - } - } - - func deleteWebPage(_ id: String) async throws { - logger.info("Requesting web page deletion: \(id)") - - guard Auth.auth().currentUser?.uid != nil else { - logger.error("User not authenticated") - throw DataLayerError.notAuthenticated - } - - do { - try await FunctionAPIClient.shared.send( - .requestWebPageDeletion(id) - ) - logger.info("Successfully requested web page deletion") - } catch { - logger.error("Failed to request web page deletion", error: error) - record(error, code: .deleteWebPage) - throw error - } - } - - func undoDeleteWebPage(_ id: String) async throws { - logger.info("Undoing web page deletion: \(id)") - - guard Auth.auth().currentUser?.uid != nil else { - logger.error("User not authenticated") - throw DataLayerError.notAuthenticated - } - - do { - try await FunctionAPIClient.shared.send( - .undoWebPageDeletion(id) - ) - logger.info("Successfully undone web page deletion") - } catch { - logger.error("Failed to undo web page deletion", error: error) - record(error, code: .undoDeleteWebPage) - throw error - } - } -} - -private extension WebPageServiceImpl { - private static func record(_ error: Error, code: CrashlyticsError.Code) { - FirebaseCrashlyticsHelper.record( - error, - domain: "\(CrashlyticsError.domain).\(code)", - code: code.rawValue - ) - } - - private func record(_ error: Error, code: CrashlyticsError.Code) { - Self.record(error, code: code) - } - - func documentID(for url: String) -> String { - if let encoded = url.addingPercentEncoding(withAllowedCharacters: .alphanumerics) { - return encoded - } - let base64 = Data(url.utf8).base64EncodedString() - return base64 - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "=", with: "") - } - - func makeResponse(from snapshot: QueryDocumentSnapshot) -> WebPageResponse? { - let data = snapshot.data() - guard - (data[WebPageFieldKey.isDeleted.rawValue] as? Bool) != true, - let title = data[WebPageFieldKey.title.rawValue] as? String, - let url = data[WebPageFieldKey.url.rawValue] as? String, - let displayURL = data[WebPageFieldKey.displayURL.rawValue] as? String, - let imageURL = data[WebPageFieldKey.imageURL.rawValue] as? String else { - return nil - } - - return WebPageResponse( - id: snapshot.documentID, - title: title, - url: url, - displayURL: displayURL, - imageURL: imageURL - ) - } - - enum WebPageFieldKey: String { - case title - case url - case displayURL - case imageURL - case isDeleted // 삭제 요청으로 서버에서 soft deletion이 된 상태 - } -} diff --git a/Application/Persistence/Sources/Graph/WebPageImageStoreGraph.swift b/Application/Persistence/Sources/Graph/WebPageImageStoreGraph.swift deleted file mode 100644 index 2d3f3f0a..00000000 --- a/Application/Persistence/Sources/Graph/WebPageImageStoreGraph.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// WebPageImageStoreGraph.swift -// Persistence -// -// Created by opfic on 9/7/26. -// - -import Cradle -import Data - -@DependencyGraph -public final class WebPageImageStoreGraph { - public init() { } - - @Provide - private func makeWebPageImageStore() -> WebPageImageStore { - WebPageImageStoreImpl() - } -} diff --git a/Application/Persistence/Sources/Persistence/WebPageImageStoreImpl.swift b/Application/Persistence/Sources/Persistence/WebPageImageStoreImpl.swift deleted file mode 100644 index 8abd05bd..00000000 --- a/Application/Persistence/Sources/Persistence/WebPageImageStoreImpl.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// WebPageImageStoreImpl.swift -// Persistence -// -// Created by opfic on 4/14/26. -// - -import CryptoKit -import Foundation -import Data - -final class WebPageImageStoreImpl: WebPageImageStore { - private let queue = DispatchQueue( - label: "devlog.web-page-image-store", - qos: .utility - ) - - func cachedImageURL(for url: URL, accountID: String? = nil) async throws -> URL { - return try await perform { - try Self.cachedImageURL(for: url, accountID: accountID) - } - } - - func saveImage(_ data: Data, for url: URL, accountID: String? = nil) async throws -> URL { - return try await perform { - try Self.saveImage(data, for: url, accountID: accountID) - } - } - - func dirSizeInBytes(accountID: String? = nil) async -> Int64 { - do { - return try await perform { - try Self.dirSizeInBytes(accountID: accountID) - } - } catch { - return 0 - } - } - - func clearDirectory(accountID: String? = nil) async throws { - try await perform { - try Self.clearDirectory(accountID: accountID) - } - } - - func removeImage(for url: URL, accountID: String? = nil) async throws -> Bool { - return try await perform { - try Self.removeImage(for: url, accountID: accountID) - } - } -} - -private extension WebPageImageStoreImpl { - func perform(_ operation: @escaping @Sendable () throws -> T) async throws -> T { - try await withCheckedThrowingContinuation { continuation in - queue.async { - do { - continuation.resume(returning: try operation()) - } catch { - continuation.resume(throwing: error) - } - } - } - } - - static func hashedFileName(for url: URL) -> String { - let hashValue = SHA256.hash(data: Data(url.absoluteString.utf8)) - return hashValue.map { String(format: "%02x", $0) }.joined() - } - - static func cachedImageURL(for url: URL, accountID: String?) throws -> URL { - let fileManager = FileManager.default - let imageDirectoryURL = try imageDirectoryURL( - accountID: accountID, - create: true, - fileManager: fileManager - ) - let fileName = hashedFileName(for: url) - - return imageDirectoryURL - .appendingPathComponent(fileName) - .appendingPathExtension("jpeg") - } - - static func saveImage(_ data: Data, for url: URL, accountID: String?) throws -> URL { - let fileURL = try cachedImageURL(for: url, accountID: accountID) - try data.write(to: fileURL, options: [.atomic]) - return fileURL - } - - static func dirSizeInBytes(accountID: String?) throws -> Int64 { - let fileManager = FileManager.default - let imageDirectoryURL = try imageDirectoryURL( - accountID: accountID, - create: false, - fileManager: fileManager - ) - guard fileManager.fileExists(atPath: imageDirectoryURL.path) else { return 0 } - guard let enumerator = fileManager.enumerator( - at: imageDirectoryURL, - includingPropertiesForKeys: [.isRegularFileKey, .fileSizeKey], - options: [.skipsHiddenFiles] - ) else { - return 0 - } - - var total: Int64 = 0 - for case let fileURL as URL in enumerator { - guard let resourceValues = try? fileURL.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey]), - resourceValues.isRegularFile == true, - let fileSize = resourceValues.fileSize else { - continue - } - total += Int64(fileSize) - } - return total - } - - static func clearDirectory(accountID: String?) throws { - let fileManager = FileManager.default - let imageDirectoryURL = try imageDirectoryURL( - accountID: accountID, - create: false, - fileManager: fileManager - ) - guard fileManager.fileExists(atPath: imageDirectoryURL.path) else { return } - let contentURLs = try fileManager.contentsOfDirectory( - at: imageDirectoryURL, - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) - for contentURL in contentURLs { - try fileManager.removeItem(at: contentURL) - } - } - - static func removeImage(for url: URL, accountID: String?) throws -> Bool { - let fileManager = FileManager.default - let fileURL = try cachedImageURL(for: url, accountID: accountID) - guard fileManager.fileExists(atPath: fileURL.path) else { return false } - try fileManager.removeItem(at: fileURL) - return true - } - - static func imageDirectoryURL(accountID: String?, create: Bool, fileManager: FileManager) throws -> URL { - let directory = try fileManager.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: create - ) - let imageBaseDirectory = directory.appendingPathComponent("webPageImages", isDirectory: true) - let imageDirectory = accountImageDirectoryURL( - in: imageBaseDirectory, - accountID: accountID - ) - - if create && !fileManager.fileExists(atPath: imageDirectory.path) { - try fileManager.createDirectory(at: imageDirectory, withIntermediateDirectories: true) - } - if create { - var resourceValues = URLResourceValues() - resourceValues.isExcludedFromBackup = true - var imageDirectory = imageDirectory - try imageDirectory.setResourceValues(resourceValues) - } - - return imageDirectory - } - - static func accountImageDirectoryURL(in imageBaseDirectory: URL, accountID: String?) -> URL { - guard let accountID = normalizedAccountID(accountID) else { - return imageBaseDirectory - } - - return imageBaseDirectory.appendingPathComponent( - hashedDirectoryName(for: accountID), - isDirectory: true - ) - } - - static func normalizedAccountID(_ accountID: String?) -> String? { - guard let accountID = accountID?.trimmingCharacters(in: .whitespacesAndNewlines), - !accountID.isEmpty else { - return nil - } - return accountID - } - - static func hashedDirectoryName(for accountID: String) -> String { - let hashValue = SHA256.hash(data: Data(accountID.utf8)) - return hashValue.map { String(format: "%02x", $0) }.joined() - } -} diff --git a/Application/Persistence/Tests/Persistence/WebPageImageStoreImplTests.swift b/Application/Persistence/Tests/Persistence/WebPageImageStoreImplTests.swift deleted file mode 100644 index edbef27e..00000000 --- a/Application/Persistence/Tests/Persistence/WebPageImageStoreImplTests.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// WebPageImageStoreImplTests.swift -// PersistenceTests -// -// Created by opfic on 6/3/26. -// - -import Foundation -import Testing -@testable import Persistence - -@Suite(.serialized) -struct WebPageImageStoreImplTests { - @Test("웹페이지 이미지 캐시는 계정별로 분리되고 현재 계정 삭제는 다른 계정을 유지한다") - func 웹페이지_이미지_캐시는_계정별로_분리되고_현재_계정_삭제는_다른_계정을_유지한다() async throws { - let store = WebPageImageStoreImpl() - let fileManager = FileManager.default - let url = try #require(URL(string: "https://example.com/image")) - let firstAccountID = "account-a" - let secondAccountID = "account-b" - - try await store.clearDirectory(accountID: firstAccountID) - try await store.clearDirectory(accountID: secondAccountID) - - let firstFileURL = try await store.saveImage( - Data("first-account".utf8), - for: url, - accountID: firstAccountID - ) - let secondFileURL = try await store.saveImage( - Data("second-account".utf8), - for: url, - accountID: secondAccountID - ) - - #expect(firstFileURL.lastPathComponent == secondFileURL.lastPathComponent) - #expect(firstFileURL.deletingLastPathComponent() != secondFileURL.deletingLastPathComponent()) - #expect(fileManager.fileExists(atPath: firstFileURL.path)) - #expect(fileManager.fileExists(atPath: secondFileURL.path)) - - try await store.clearDirectory(accountID: firstAccountID) - - let firstDirectorySize = await store.dirSizeInBytes(accountID: firstAccountID) - let secondDirectorySize = await store.dirSizeInBytes(accountID: secondAccountID) - #expect(firstDirectorySize == 0) - #expect(0 < secondDirectorySize) - #expect(!fileManager.fileExists(atPath: firstFileURL.path)) - #expect(fileManager.fileExists(atPath: secondFileURL.path)) - - try await store.clearDirectory(accountID: secondAccountID) - } -} diff --git a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings index 8d930fde..c44a87fd 100644 --- a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings +++ b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings @@ -1658,40 +1658,6 @@ } } }, - "settings_alert_clear_temp_message" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This will clear temporary data and free up space.\nDo you want to continue?" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "임시 데이터를 삭제하고 정리합니다.\n계속하시겠습니까?" - } - } - } - }, - "settings_alert_clear_temp_title" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear Temporary Data" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "임시 데이터 삭제" - } - } - } - }, "settings_alert_delete_account_message" : { "extractionState" : "manual", "localizations" : { @@ -1760,23 +1726,6 @@ } } }, - "settings_clear_temp_data" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear Temporary Data" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "임시 데이터 삭제" - } - } - } - }, "settings_delete_account" : { "extractionState" : "manual", "localizations" : { diff --git a/Application/Presentation/ProfileTab/Sources/Profile/ProfileDependencyPreparation.swift b/Application/Presentation/ProfileTab/Sources/Profile/ProfileDependencyPreparation.swift index 78700732..c0b38830 100644 --- a/Application/Presentation/ProfileTab/Sources/Profile/ProfileDependencyPreparation.swift +++ b/Application/Presentation/ProfileTab/Sources/Profile/ProfileDependencyPreparation.swift @@ -60,15 +60,6 @@ public enum ProfileDependencyPreparation { dependencies.updateSystemThemeUseCase = updateSystemThemeUseCase } - public static func prepareSettingsStorage( - _ dependencies: inout DependencyValues, - fetchWebPageImageDirSizeUseCase: FetchWebPageImageDirSizeUseCase, - clearWebPageImageDirectoryUseCase: ClearWebPageImageDirectoryUseCase - ) { - dependencies.fetchWebPageImageDirSizeUseCase = fetchWebPageImageDirSizeUseCase - dependencies.clearWebPageImageDirectoryUseCase = clearWebPageImageDirectoryUseCase - } - public static func prepareAccount( _ dependencies: inout DependencyValues, fetchAuthProvidersUseCase: FetchAuthProvidersUseCase, diff --git a/Application/Presentation/ProfileTab/Sources/Settings/SettingsFeature.swift b/Application/Presentation/ProfileTab/Sources/Settings/SettingsFeature.swift index d479ab65..6251d45a 100644 --- a/Application/Presentation/ProfileTab/Sources/Settings/SettingsFeature.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/SettingsFeature.swift @@ -19,7 +19,6 @@ struct SettingsFeature { } enum ActiveLoadingRow: Equatable { - case removeCache case signOut case deleteAuth } @@ -28,7 +27,6 @@ struct SettingsFeature { struct State: Equatable { @Presents var alert: AlertState? var theme: SystemTheme = .automatic - var dirSize: Int64 = 0 var isNetworkConnected = true var activeLoadingRow: ActiveLoadingRow? var loading = LoadingFeature.State() @@ -75,22 +73,17 @@ struct SettingsFeature { case startObserving case networkStatusChanged(Bool) case setAlert(AlertType) - case setDirSize(Int64) - case updateDirSize - case tapRemoveCacheButton case loading(LoadingFeature.Action) enum Alert: Equatable { case tapDeleteAuthButton case tapSignOutButton - case confirmRemoveCache } enum AlertType: Equatable { case signOut case deleteAuth case error - case removeCache } } @@ -99,8 +92,6 @@ struct SettingsFeature { @Dependency(\.profileNetworkConnectivityUseCase) var networkConnectivityUseCase @Dependency(\.profileSystemThemeUseCase) var systemThemeUseCase @Dependency(\.updateSystemThemeUseCase) var updateSystemThemeUseCase - @Dependency(\.fetchWebPageImageDirSizeUseCase) var fetchWebPageImageDirSizeUseCase - @Dependency(\.clearWebPageImageDirectoryUseCase) var clearWebPageImageDirectoryUseCase var body: some ReducerOf { Scope(state: \.loading, action: \.loading) { @@ -119,11 +110,6 @@ struct SettingsFeature { state.alertType = nil state.activeLoadingRow = .signOut return signOutEffect() - case .alert(.presented(.confirmRemoveCache)): - state.alert = nil - state.alertType = nil - state.activeLoadingRow = .removeCache - return clearWebPageImageDirectoryEffect() case .alert(.dismiss): state.alert = nil state.alertType = nil @@ -143,13 +129,6 @@ struct SettingsFeature { case .setAlert(let type): state.alert = Self.alertState(for: type) state.alertType = type - case .setDirSize(let value): - state.dirSize = value - case .updateDirSize: - return fetchWebPageImageDirSizeEffect() - case .tapRemoveCacheButton: - state.alert = Self.alertState(for: .removeCache) - state.alertType = .removeCache case .loading(.end): if !state.isLoading { state.activeLoadingRow = nil @@ -190,15 +169,6 @@ extension DependencyValues { set { self[UpdateSystemThemeUseCaseKey.self] = newValue } } - var fetchWebPageImageDirSizeUseCase: FetchWebPageImageDirSizeUseCase { - get { self[FetchWebPageImageDirSizeUseCaseKey.self] } - set { self[FetchWebPageImageDirSizeUseCaseKey.self] = newValue } - } - - var clearWebPageImageDirectoryUseCase: ClearWebPageImageDirectoryUseCase { - get { self[ClearWebPageImageDirectoryUseCaseKey.self] } - set { self[ClearWebPageImageDirectoryUseCaseKey.self] = newValue } - } } private enum DeleteAuthUseCaseKey: DependencyKey { @@ -251,26 +221,6 @@ private enum UpdateSystemThemeUseCaseKey: DependencyKey { } } -private enum FetchWebPageImageDirSizeUseCaseKey: DependencyKey { - static var liveValue: FetchWebPageImageDirSizeUseCase { - preconditionFailure("FetchWebPageImageDirSizeUseCase must be provided.") - } - - static var testValue: FetchWebPageImageDirSizeUseCase { - liveValue - } -} - -private enum ClearWebPageImageDirectoryUseCaseKey: DependencyKey { - static var liveValue: ClearWebPageImageDirectoryUseCase { - preconditionFailure("ClearWebPageImageDirectoryUseCase must be provided.") - } - - static var testValue: ClearWebPageImageDirectoryUseCase { - liveValue - } -} - private extension SettingsFeature { func observeNetworkConnectivityEffect() -> Effect { .publisher { [networkConnectivityUseCase] in @@ -295,28 +245,6 @@ private extension SettingsFeature { } } - func fetchWebPageImageDirSizeEffect() -> Effect { - .run { [fetchWebPageImageDirSizeUseCase] send in - let dirSize = await fetchWebPageImageDirSizeUseCase.execute() - await send(.setDirSize(dirSize)) - } - } - - func clearWebPageImageDirectoryEffect() -> Effect { - .run { [clearWebPageImageDirectoryUseCase, fetchWebPageImageDirSizeUseCase] send in - await send(.loading(.begin(target: .default, mode: .delayed))) - do { - try await clearWebPageImageDirectoryUseCase.execute() - let dirSize = await fetchWebPageImageDirSizeUseCase.execute() - await send(.setDirSize(dirSize)) - await send(.loading(.end(target: .default, mode: .delayed))) - } catch { - await send(.loading(.end(target: .default, mode: .delayed))) - await send(.setAlert(.error)) - } - } - } - func deleteAuthEffect() -> Effect { .run { [deleteAuthUseCase] send in await send(.loading(.begin(target: .default, mode: .delayed))) @@ -391,19 +319,6 @@ private extension SettingsFeature { } message: { TextState(String(localized: "common_error_message", bundle: PresentationResources.bundle)) } - case .removeCache: - return AlertState { - TextState(String(localized: "settings_alert_clear_temp_title", bundle: PresentationResources.bundle)) - } actions: { - ButtonState(role: .cancel) { - TextState(String(localized: "common_cancel", bundle: PresentationResources.bundle)) - } - ButtonState(role: .destructive, action: .confirmRemoveCache) { - TextState(String(localized: "common_confirm", bundle: PresentationResources.bundle)) - } - } message: { - TextState(String(localized: "settings_alert_clear_temp_message", bundle: PresentationResources.bundle)) - } } } } diff --git a/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift b/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift index 714abfa8..6a8e2d3e 100644 --- a/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift @@ -37,24 +37,6 @@ struct SettingsView: View { } .disabled(!connected) - let dirSize = store.dirSize - Button { - store.send(.tapRemoveCacheButton) - } label: { - HStack { - Text(String(localized: "settings_clear_temp_data", bundle: PresentationResources.bundle)) - .foregroundStyle(dirSize == 0 ? Color.secondary : .primary) - Spacer() - if store.activeLoadingRow == .removeCache { - ProgressView() - .tint(.secondary) - } else { - Text(formatFileSize(bytes: dirSize)) - .foregroundStyle(Color.secondary.opacity(dirSize == 0 ? 0 : 1)) - } - } - } - .disabled(dirSize == 0 || store.isLoading) } Section { @@ -128,25 +110,5 @@ struct SettingsView: View { .navigationTitle(String(localized: "nav_settings", bundle: PresentationResources.bundle)) .navigationBarTitleDisplayMode(.inline) .prominentAlert(store, state: \.alert, action: \.alert) - .onAppear { - store.send(.updateDirSize) - } - } - - private func formatFileSize(bytes: Int64) -> String { - let units = ["B", "KB", "MB", "GB"] - var value = Double(max(bytes, 0)) - var unitIndex = 0 - - while 1024.0 <= value && unitIndex < units.count - 1 { - value /= 1024.0 - unitIndex += 1 - } - - let truncated = floor(value * 100.0) / 100.0 - let numberString = truncated.formatted( - .number.precision(.fractionLength(0...2)) - ) - return "\(numberString)\(units[unitIndex])" } } diff --git a/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTestDoubles.swift b/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTestDoubles.swift index 79fca2ab..77eef9c3 100644 --- a/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTestDoubles.swift +++ b/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTestDoubles.swift @@ -97,33 +97,6 @@ final class UpdateSystemThemeUseCaseSpy: UpdateSystemThemeUseCase { } } -final class FetchWebPageImageDirSizeUseCaseSpy: FetchWebPageImageDirSizeUseCase { - var dirSize: Int64 - private(set) var executeCallCount = 0 - - init(dirSize: Int64 = 0) { - self.dirSize = dirSize - } - - func execute() async -> Int64 { - executeCallCount += 1 - return dirSize - } -} - -final class ClearWebPageImageDirectoryUseCaseSpy: ClearWebPageImageDirectoryUseCase { - var error: Error? - private(set) var executeCallCount = 0 - - func execute() async throws { - executeCallCount += 1 - - if let error { - throw error - } - } -} - enum SettingsTestError: Error { case failure } diff --git a/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTests.swift b/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTests.swift index 3662e8a0..42631932 100644 --- a/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTests.swift +++ b/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTests.swift @@ -84,67 +84,6 @@ struct SettingsFeatureTests { #expect(updateSpy.themes == [.light]) } - @Test("캐시 크기 조회 결과를 상태에 반영한다") - func 캐시_크기_조회_결과를_상태에_반영한다() async { - let fetchSpy = FetchWebPageImageDirSizeUseCaseSpy(dirSize: 2_048) - let adapter = SettingsStoreTestAdapter(fetchDirSizeUseCase: fetchSpy) - - await adapter.updateDirSize() - - #expect(fetchSpy.executeCallCount == 1) - #expect(adapter.dirSize == 2_048) - } - - @Test("캐시 삭제를 누르면 삭제 확인 알림을 표시한다") - func 캐시_삭제를_누르면_삭제_확인_알림을_표시한다() async { - let adapter = SettingsStoreTestAdapter() - - await adapter.tapRemoveCacheButton() - - #expect(adapter.showAlert) - #expect(adapter.alertType == .removeCache) - #expect( - adapter.alertTitle - == String(localized: "settings_alert_clear_temp_title", bundle: PresentationResources.bundle) - ) - #expect( - adapter.alertMessage - == String(localized: "settings_alert_clear_temp_message", bundle: PresentationResources.bundle) - ) - } - - @Test("캐시 삭제 확인에 성공하면 캐시를 비우고 크기를 다시 조회한다") - func 캐시_삭제_확인에_성공하면_캐시를_비우고_크기를_다시_조회한다() async { - let clearSpy = ClearWebPageImageDirectoryUseCaseSpy() - let fetchSpy = FetchWebPageImageDirSizeUseCaseSpy(dirSize: 0) - let adapter = SettingsStoreTestAdapter( - fetchDirSizeUseCase: fetchSpy, - clearDirectoryUseCase: clearSpy - ) - - await adapter.tapRemoveCacheButton() - await adapter.confirmRemoveCache() - - #expect(!adapter.showAlert) - #expect(adapter.dirSize == 0) - #expect(adapter.activeLoadingRow == nil) - } - - @Test("캐시 삭제에 실패하면 공통 에러 알림을 표시한다") - func 캐시_삭제에_실패하면_공통_에러_알림을_표시한다() async { - let clearSpy = ClearWebPageImageDirectoryUseCaseSpy() - clearSpy.error = SettingsTestError.failure - let adapter = SettingsStoreTestAdapter(clearDirectoryUseCase: clearSpy) - - await adapter.tapRemoveCacheButton() - await adapter.confirmRemoveCache() - - #expect(adapter.showAlert) - #expect(adapter.alertTitle == String(localized: "common_error_title", bundle: PresentationResources.bundle)) - #expect(adapter.alertMessage == String(localized: "common_error_message", bundle: PresentationResources.bundle)) - #expect(adapter.activeLoadingRow == nil) - } - @Test("로그아웃 성공 후에도 LoginView 전환 전까지 로딩 상태를 유지한다") func 로그아웃_성공_후에도_LoginView_전환_전까지_로딩_상태를_유지한다() async { let signOutSpy = SignOutUseCaseSpy() @@ -198,7 +137,6 @@ private enum SettingsAlertType { case signOut case deleteAuth case error - case removeCache } @MainActor @@ -207,7 +145,6 @@ private struct SettingsStoreTestAdapter { private let clock: TestClock var theme: SystemTheme { store.state.theme } - var dirSize: Int64 { store.state.dirSize } var isNetworkConnected: Bool { store.state.isNetworkConnected } var isLoading: Bool { store.state.isLoading } var activeLoadingRow: SettingsFeature.ActiveLoadingRow? { store.state.activeLoadingRow } @@ -229,9 +166,7 @@ private struct SettingsStoreTestAdapter { signOutUseCase: SignOutUseCase = SignOutUseCaseSpy(), networkUseCase: ObserveNetworkConnectivityUseCase = ObserveNetworkConnectivityUseCaseSpy(), themeUseCase: ObserveSystemThemeUseCase = ObserveSystemThemeUseCaseSpy(), - updateThemeUseCase: UpdateSystemThemeUseCase = UpdateSystemThemeUseCaseSpy(), - fetchDirSizeUseCase: FetchWebPageImageDirSizeUseCase = FetchWebPageImageDirSizeUseCaseSpy(), - clearDirectoryUseCase: ClearWebPageImageDirectoryUseCase = ClearWebPageImageDirectoryUseCaseSpy() + updateThemeUseCase: UpdateSystemThemeUseCase = UpdateSystemThemeUseCaseSpy() ) { let clock = TestClock() self.clock = clock @@ -243,8 +178,6 @@ private struct SettingsStoreTestAdapter { $0.profileNetworkConnectivityUseCase = networkUseCase $0.profileSystemThemeUseCase = themeUseCase $0.updateSystemThemeUseCase = updateThemeUseCase - $0.fetchWebPageImageDirSizeUseCase = fetchDirSizeUseCase - $0.clearWebPageImageDirectoryUseCase = clearDirectoryUseCase $0.continuousClock = clock } store.exhaustivity = .off(showSkippedAssertions: false) @@ -260,27 +193,6 @@ private struct SettingsStoreTestAdapter { } } - func updateDirSize() async { - await store.send(.updateDirSize) - await drainReceivedActions() - } - - func tapRemoveCacheButton() async { - await store.send(.tapRemoveCacheButton) { - $0.alert = expectedSettingsAlert(for: .removeCache) - $0.alertType = .removeCache - } - } - - func confirmRemoveCache() async { - await store.send(.alert(.presented(.confirmRemoveCache))) { - $0.alert = nil - $0.alertType = nil - $0.activeLoadingRow = .removeCache - } - await drainReceivedActions() - } - func tapSignOutButton() async { await store.send(.setAlert(.signOut)) { $0.alert = expectedSettingsAlert(for: .signOut) @@ -335,8 +247,6 @@ private extension SettingsAlertType { self = .deleteAuth case .error: self = .error - case .removeCache: - self = .removeCache } } } @@ -381,18 +291,5 @@ private func expectedSettingsAlert( } message: { TextState(String(localized: "common_error_message", bundle: PresentationResources.bundle)) } - case .removeCache: - return AlertState { - TextState(String(localized: "settings_alert_clear_temp_title", bundle: PresentationResources.bundle)) - } actions: { - ButtonState(role: .cancel) { - TextState(String(localized: "common_cancel", bundle: PresentationResources.bundle)) - } - ButtonState(role: .destructive, action: .confirmRemoveCache) { - TextState(String(localized: "common_confirm", bundle: PresentationResources.bundle)) - } - } message: { - TextState(String(localized: "settings_alert_clear_temp_message", bundle: PresentationResources.bundle)) - } } } diff --git a/README.md b/README.md index 7af41d9a..8833ca2e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # DevLog > 개발 기록과 Todo를 한 곳에서 관리하는 SwiftUI 기반 앱 -> 저장한 링크, 작업 메모, 마감 일정, 개인 활동 흐름을 하나의 앱 안에서 정리하는 구조 +> 작업 메모, 마감 일정, 개인 활동 흐름을 하나의 앱 안에서 정리하는 구조 @@ -53,12 +53,10 @@ ## 프로젝트 개요 -개발 과정에서 해야 할 일, 참고 링크, 진행 기록이 여러 곳에 흩어지기 쉬운 문제 해결 목적 -Todo, 저장 링크, 오늘 할 일, 받은 알림, 누적 활동을 하나의 화면 흐름 안에서 함께 관리할 수 있도록 구성한 앱 +개발 과정에서 해야 할 일과 진행 기록이 여러 곳에 흩어지기 쉬운 문제를 해결하고 Todo와 오늘 할 일, 받은 알림, 누적 활동을 하나의 화면 흐름 안에서 함께 관리할 수 있도록 구성한 앱 - Todo 유형별 정리 및 빠른 탐색 - Markdown, 태그, 마감일, 중요 표시를 포함한 Todo 작성 -- 웹 페이지 저장 및 재열람 - 오늘 기준 우선 확인 Todo 요약 - 받은 푸시 알림 확인 및 Todo 연계 - 분기별 활동 히트맵 제공 @@ -101,8 +99,6 @@ Todo, 저장 링크, 오늘 할 일, 받은 알림, 누적 활동을 하나의 - 작업 성격별 Todo 유형 진입점 제공 - Home에서 Todo 유형 노출 여부 및 순서 편집 - 최근 수정 Todo 별도 섹션 제공 -- 저장한 웹 페이지 목록 확인 및 즉시 열람 -- URL 입력 시 메타데이터 수집 후 제목과 썸네일 저장 ### Todo 관리 @@ -130,7 +126,7 @@ Todo, 저장 링크, 오늘 할 일, 받은 알림, 누적 활동을 하나의 ### 검색 - Home 화면 검색 버튼을 통한 통합 검색 진입 -- Todo와 저장한 웹 페이지 통합 검색 +- Todo 검색 - 디바운스 기반 검색 처리 - 최근 검색어 저장, 개별 삭제, 전체 삭제 지원 @@ -138,7 +134,7 @@ Todo, 저장 링크, 오늘 할 일, 받은 알림, 누적 활동을 하나의 - 상태 메시지 직접 수정 - 분기 이동 및 직접 선택, 생성/완료 활동 필터 기반 히트맵 제공 -- 테마 변경, 푸시 알림 시간 설정, 캐시 정리 기능 제공 +- 테마 변경과 푸시 알림 시간 설정 기능 제공 - 설정 화면에서 앱 버전, 개인정보 처리방침, 베타 테스트 링크 확인 --- @@ -154,7 +150,7 @@ Todo, 저장 링크, 오늘 할 일, 받은 알림, 누적 활동을 하나의 | State & Async | Observable, Combine, async/await, The Composable Architecture | | Backend | Firebase Authentication, Firestore, Cloud Functions, Cloud Messaging | | Monitoring | Firebase Analytics, Crashlytics | -| Apple Frameworks | AuthenticationServices, UserNotifications, LinkPresentation, Network, CryptoKit, os.log | +| Apple Frameworks | AuthenticationServices, UserNotifications, Network, CryptoKit, os.log | | External Packages | ComposableArchitecture, OrderedCollections, GoogleSignIn, Nexa | | Testing | swift-testing, TCA TestStore | | Tooling | Xcode, Tuist, mise, Swift Package Manager, SwiftLint, Fastlane |