From 85a2b943fc6fcaf68530e14f76e61026cdf0c9c2 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 13:28:54 +0900 Subject: [PATCH 01/11] =?UTF-8?q?refactor:=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20.cache=EC=97=90=EC=84=9C=20.applicationSup?= =?UTF-8?q?port=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Infra/Service/WebPageMetadataService.swift | 7 +++---- .../ViewModel/SettingViewModel.swift | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/DevLog/Infra/Service/WebPageMetadataService.swift b/DevLog/Infra/Service/WebPageMetadataService.swift index db35e3ad..d47c29e0 100644 --- a/DevLog/Infra/Service/WebPageMetadataService.swift +++ b/DevLog/Infra/Service/WebPageMetadataService.swift @@ -104,7 +104,6 @@ final class WebPageMetadataService { private static func cacheFileURL(for url: URL) throws -> URL { let imageDir = try imageDirectoryURL() - let fileName = url.absoluteString .addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? UUID().uuidString @@ -114,13 +113,13 @@ final class WebPageMetadataService { } private static func imageDirectoryURL() throws -> URL { - let cachesDir = try FileManager.default.url( - for: .cachesDirectory, + let directory = try FileManager.default.url( + for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) - let imageDir = cachesDir.appendingPathComponent("webPageImages", isDirectory: true) + let imageDir = directory.appendingPathComponent("webPageImages", isDirectory: true) if !FileManager.default.fileExists(atPath: imageDir.path) { try FileManager.default.createDirectory(at: imageDir, withIntermediateDirectories: true) } diff --git a/DevLog/Presentation/ViewModel/SettingViewModel.swift b/DevLog/Presentation/ViewModel/SettingViewModel.swift index 482b47d5..e705a0ab 100644 --- a/DevLog/Presentation/ViewModel/SettingViewModel.swift +++ b/DevLog/Presentation/ViewModel/SettingViewModel.swift @@ -183,14 +183,15 @@ private extension SettingViewModel { func dirSizeInBytes() -> Int64 { do { - let cachesDir = try FileManager.default.url( - for: .cachesDirectory, + let directory = try FileManager.default.url( + for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false ) - guard FileManager.default.fileExists(atPath: cachesDir.path) else { return 0 } - return directorySize(at: cachesDir) + let imageDir = directory.appendingPathComponent("webPageImages", isDirectory: true) + guard FileManager.default.fileExists(atPath: imageDir.path) else { return 0 } + return directorySize(at: imageDir) } catch { return 0 } @@ -231,15 +232,16 @@ private extension SettingViewModel { } private func clearCacheDirectory() throws { - let cachesDir = try FileManager.default.url( - for: .cachesDirectory, + let directory = try FileManager.default.url( + for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false ) - guard FileManager.default.fileExists(atPath: cachesDir.path) else { return } + let imageDir = directory.appendingPathComponent("webPageImages", isDirectory: true) + guard FileManager.default.fileExists(atPath: imageDir.path) else { return } let contents = try FileManager.default.contentsOfDirectory( - at: cachesDir, + at: imageDir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles] ) From e01e227b717b6d406091ff30cb1ffb613dce053b Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 16:22:09 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=EC=9B=B9=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=8D=B9=EB=84=A4=EC=9D=BC=EC=9D=84=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=EC=97=90=20CRUD=ED=95=98=EA=B3=A0,=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=90=9C=20=ED=81=AC=EA=B8=B0=EB=A5=BC=20=EB=B0=A9?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EB=8A=94=20Store=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Assembler/PersistenceAssembler.swift | 4 + .../Persistence/WebPageImageStore.swift | 106 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 DevLog/Storage/Persistence/WebPageImageStore.swift diff --git a/DevLog/App/Assembler/PersistenceAssembler.swift b/DevLog/App/Assembler/PersistenceAssembler.swift index c904a1ec..70bd6ced 100644 --- a/DevLog/App/Assembler/PersistenceAssembler.swift +++ b/DevLog/App/Assembler/PersistenceAssembler.swift @@ -14,5 +14,9 @@ final class PersistenceAssembler: Assembler { container.register(ThemeStore.self) { ThemeStore() } + + container.register(WebPageImageStore.self) { + WebPageImageStore() + } } } diff --git a/DevLog/Storage/Persistence/WebPageImageStore.swift b/DevLog/Storage/Persistence/WebPageImageStore.swift new file mode 100644 index 00000000..df5d9d52 --- /dev/null +++ b/DevLog/Storage/Persistence/WebPageImageStore.swift @@ -0,0 +1,106 @@ +// +// WebPageImageStore.swift +// DevLog +// +// Created by opfic on 4/14/26. +// + +import Combine +import Foundation + +final class WebPageImageStore { + private let fileManager: FileManager + private let subject = CurrentValueSubject(0) + + init(fileManager: FileManager = .default) { + self.fileManager = fileManager + subject.send(dirSizeInBytes()) + } + + func cachedImageURL(for url: URL) throws -> URL { + let imageDirectoryURL = try self.imageDirectoryURL(create: true) + let fileName = url.absoluteString + .addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? UUID().uuidString + + return imageDirectoryURL + .appendingPathComponent(fileName) + .appendingPathExtension("jpeg") + } + + func saveImage(_ data: Data, for url: URL) throws -> URL { + let fileURL = try cachedImageURL(for: url) + try data.write(to: fileURL, options: [.atomic]) + subject.send(dirSizeInBytes()) + return fileURL + } + + func dirSizeInBytes() -> Int64 { + do { + let imageDirectoryURL = try self.imageDirectoryURL(create: false) + 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 + } catch { + return 0 + } + } + + func observeDirSize() -> AnyPublisher { + subject.eraseToAnyPublisher() + } + + func clearDirectory() throws { + let imageDirectoryURL = try self.imageDirectoryURL(create: false) + 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) + } + subject.send(dirSizeInBytes()) + } + + func removeImage(for url: URL) throws -> Bool { + let fileURL = try cachedImageURL(for: url) + guard fileManager.fileExists(atPath: fileURL.path) else { return false } + try fileManager.removeItem(at: fileURL) + subject.send(dirSizeInBytes()) + return true + } +} + +private extension WebPageImageStore { + func imageDirectoryURL(create: Bool) throws -> URL { + let directory = try fileManager.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: create + ) + let imageDirectory = directory.appendingPathComponent("webPageImages", isDirectory: true) + if create && !fileManager.fileExists(atPath: imageDirectory.path) { + try fileManager.createDirectory(at: imageDirectory, withIntermediateDirectories: true) + } + + return imageDirectory + } +} From 38f6e979204358bfe9b0d785e69fdf11c0a82655 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 16:23:00 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20WebPageImageStore=EA=B3=BC=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=EB=90=98=EB=8A=94=20=EB=A6=AC=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/DataAssembler.swift | 6 +++++ .../WebPageImageRepositoryImpl.swift | 22 +++++++++++++++++++ .../Protocol/WebPageImageRepository.swift | 11 ++++++++++ 3 files changed, 39 insertions(+) create mode 100644 DevLog/Data/Repository/WebPageImageRepositoryImpl.swift create mode 100644 DevLog/Domain/Protocol/WebPageImageRepository.swift diff --git a/DevLog/App/Assembler/DataAssembler.swift b/DevLog/App/Assembler/DataAssembler.swift index b193137a..aaba3440 100644 --- a/DevLog/App/Assembler/DataAssembler.swift +++ b/DevLog/App/Assembler/DataAssembler.swift @@ -87,6 +87,12 @@ final class DataAssembler: Assembler { ) } + container.register(WebPageImageRepository.self) { + WebPageImageRepositoryImpl( + store: container.resolve(WebPageImageStore.self) + ) + } + container.register(UserPreferencesRepository.self) { UserPreferencesRepositoryImpl( store: container.resolve(UserDefaultsStore.self), diff --git a/DevLog/Data/Repository/WebPageImageRepositoryImpl.swift b/DevLog/Data/Repository/WebPageImageRepositoryImpl.swift new file mode 100644 index 00000000..d14d6e1a --- /dev/null +++ b/DevLog/Data/Repository/WebPageImageRepositoryImpl.swift @@ -0,0 +1,22 @@ +// +// WebPageImageRepositoryImpl.swift +// DevLog +// +// Created by opfic on 4/14/26. +// + +final class WebPageImageRepositoryImpl: WebPageImageRepository { + private let store: WebPageImageStore + + init(store: WebPageImageStore) { + self.store = store + } + + func fetchDirSizeInBytes() -> Int64 { + store.dirSizeInBytes() + } + + func clearDirectory() throws { + try store.clearDirectory() + } +} diff --git a/DevLog/Domain/Protocol/WebPageImageRepository.swift b/DevLog/Domain/Protocol/WebPageImageRepository.swift new file mode 100644 index 00000000..f7a0ee76 --- /dev/null +++ b/DevLog/Domain/Protocol/WebPageImageRepository.swift @@ -0,0 +1,11 @@ +// +// WebPageImageRepository.swift +// DevLog +// +// Created by opfic on 4/14/26. +// + +protocol WebPageImageRepository { + func fetchDirSizeInBytes() -> Int64 + func clearDirectory() throws +} From e1ac232f49ed4330b349fa11facd3f270aa504db Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 16:31:31 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EC=9B=B9=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=8D=B8=EB=84=A4=EC=9D=BC=EC=9D=98=20=EC=B4=9D=20?= =?UTF-8?q?=EC=9A=A9=EB=9F=89=EC=9D=84=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EC=9C=A0=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/DomainAssembler.swift | 4 ++++ .../FetchWebPageImageDirSizeUseCase.swift | 10 ++++++++++ .../FetchWebPageImageDirSizeUseCaseImpl.swift | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift create mode 100644 DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift diff --git a/DevLog/App/Assembler/DomainAssembler.swift b/DevLog/App/Assembler/DomainAssembler.swift index da962efc..50766734 100644 --- a/DevLog/App/Assembler/DomainAssembler.swift +++ b/DevLog/App/Assembler/DomainAssembler.swift @@ -145,6 +145,10 @@ private extension DomainAssembler { FetchWebPagesUseCaseImpl(container.resolve(WebPageRepository.self)) } + container.register(FetchWebPageImageDirSizeUseCase.self) { + FetchWebPageImageDirSizeUseCaseImpl(container.resolve(WebPageImageRepository.self)) + } + container.register(AddWebPageUseCase.self) { AddWebPageUseCaseImpl(container.resolve(WebPageRepository.self)) } diff --git a/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift b/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift new file mode 100644 index 00000000..e6807163 --- /dev/null +++ b/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift @@ -0,0 +1,10 @@ +// +// FetchWebPageImageDirSizeUseCase.swift +// DevLog +// +// Created by opfic on 4/14/26. +// + +protocol FetchWebPageImageDirSizeUseCase { + func execute() -> Int64 +} diff --git a/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift b/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift new file mode 100644 index 00000000..56c768cd --- /dev/null +++ b/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// FetchWebPageImageDirSizeUseCaseImpl.swift +// DevLog +// +// Created by opfic on 4/14/26. +// + +final class FetchWebPageImageDirSizeUseCaseImpl: FetchWebPageImageDirSizeUseCase { + private let repository: WebPageImageRepository + + init(_ repository: WebPageImageRepository) { + self.repository = repository + } + + func execute() -> Int64 { + repository.fetchDirSizeInBytes() + } +} From 2fb1e358a6d5c9902c3782015cdf4242d2b57979 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 16:31:40 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=EC=9B=B9=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=8D=B8=EB=84=A4=EC=9D=BC=EC=9D=98=20=EC=B4=9D=20?= =?UTF-8?q?=EC=9A=A9=EB=9F=89=EC=9D=84=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=9C=A0=EC=A6=88=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/DomainAssembler.swift | 4 ++++ .../ClearWebPageImageDirectoryUseCase.swift | 10 ++++++++++ ...ClearWebPageImageDirectoryUseCaseImpl.swift | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift create mode 100644 DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift diff --git a/DevLog/App/Assembler/DomainAssembler.swift b/DevLog/App/Assembler/DomainAssembler.swift index 50766734..9824e444 100644 --- a/DevLog/App/Assembler/DomainAssembler.swift +++ b/DevLog/App/Assembler/DomainAssembler.swift @@ -153,6 +153,10 @@ private extension DomainAssembler { AddWebPageUseCaseImpl(container.resolve(WebPageRepository.self)) } + container.register(ClearWebPageImageDirectoryUseCase.self) { + ClearWebPageImageDirectoryUseCaseImpl(container.resolve(WebPageImageRepository.self)) + } + container.register(DeleteWebPageUseCase.self) { DeleteWebPageUseCaseImpl(container.resolve(WebPageRepository.self)) } diff --git a/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift b/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift new file mode 100644 index 00000000..5ede29fb --- /dev/null +++ b/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift @@ -0,0 +1,10 @@ +// +// ClearWebPageImageDirectoryUseCase.swift +// DevLog +// +// Created by opfic on 4/14/26. +// + +protocol ClearWebPageImageDirectoryUseCase { + func execute() throws +} diff --git a/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift b/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift new file mode 100644 index 00000000..0f916460 --- /dev/null +++ b/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// ClearWebPageImageDirectoryUseCaseImpl.swift +// DevLog +// +// Created by opfic on 4/14/26. +// + +final class ClearWebPageImageDirectoryUseCaseImpl: ClearWebPageImageDirectoryUseCase { + private let repository: WebPageImageRepository + + init(_ repository: WebPageImageRepository) { + self.repository = repository + } + + func execute() throws { + try repository.clearDirectory() + } +} From 14f5a29b62c365e5d0762610b67e26456610aec5 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 16:31:50 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A6=88=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/SettingViewModel.swift | 70 +++---------------- DevLog/UI/Profile/ProfileView.swift | 4 +- 2 files changed, 13 insertions(+), 61 deletions(-) diff --git a/DevLog/Presentation/ViewModel/SettingViewModel.swift b/DevLog/Presentation/ViewModel/SettingViewModel.swift index e705a0ab..a9b48c97 100644 --- a/DevLog/Presentation/ViewModel/SettingViewModel.swift +++ b/DevLog/Presentation/ViewModel/SettingViewModel.swift @@ -48,6 +48,8 @@ final class SettingViewModel: Store { private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase private let systemThemeUseCase: ObserveSystemThemeUseCase private let updateSystemThemeUseCase: UpdateSystemThemeUseCase + private let fetchWebPageImageDirSizeUseCase: FetchWebPageImageDirSizeUseCase + private let clearWebPageImageDirectoryUseCase: ClearWebPageImageDirectoryUseCase private let loadingState = LoadingState() private var cancellables = Set() @@ -60,13 +62,17 @@ final class SettingViewModel: Store { signOutUseCase: SignOutUseCase, networkConnectivityUseCase: ObserveNetworkConnectivityUseCase, systemThemeUseCase: ObserveSystemThemeUseCase, - updateSystemThemeUseCase: UpdateSystemThemeUseCase + updateSystemThemeUseCase: UpdateSystemThemeUseCase, + fetchWebPageImageDirSizeUseCase: FetchWebPageImageDirSizeUseCase, + clearWebPageImageDirectoryUseCase: ClearWebPageImageDirectoryUseCase ) { self.deleteAuthuseCase = deleteAuthUseCase self.signOutUseCase = signOutUseCase self.networkConnectivityUseCase = networkConnectivityUseCase self.systemThemeUseCase = systemThemeUseCase self.updateSystemThemeUseCase = updateSystemThemeUseCase + self.fetchWebPageImageDirSizeUseCase = fetchWebPageImageDirSizeUseCase + self.clearWebPageImageDirectoryUseCase = clearWebPageImageDirectoryUseCase setupNetworkObserving() setupThemeMonitoring() } @@ -86,7 +92,7 @@ final class SettingViewModel: Store { state.theme = value updateSystemThemeUseCase.execute(value) case .updateDirSize: - state.dirSize = dirSizeInBytes() + state.dirSize = fetchWebPageImageDirSizeUseCase.execute() case .tapDeleteAuthButton: effects = [.deleteAuth] case .tapSignOutButton: @@ -96,8 +102,8 @@ final class SettingViewModel: Store { case .confirmRemoveCache: do { setAlert(&state, isPresented: false) - try clearCacheDirectory() - state.dirSize = dirSizeInBytes() + try clearWebPageImageDirectoryUseCase.execute() + state.dirSize = fetchWebPageImageDirSizeUseCase.execute() } catch { setAlert(&state, isPresented: true, type: .error) } @@ -181,44 +187,6 @@ private extension SettingViewModel { .store(in: &cancellables) } - func dirSizeInBytes() -> Int64 { - do { - let directory = try FileManager.default.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: false - ) - let imageDir = directory.appendingPathComponent("webPageImages", isDirectory: true) - guard FileManager.default.fileExists(atPath: imageDir.path) else { return 0 } - return directorySize(at: imageDir) - } catch { - return 0 - } - } - - private func directorySize(at url: URL) -> Int64 { - guard FileManager.default.fileExists(atPath: url.path) else { return 0 } - guard let enumerator = FileManager.default.enumerator( - at: url, - 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 - } - private func beginLoading(_ mode: LoadingState.Mode) { loadingState.begin(mode: mode) { [weak self] isLoading in self?.send(.setLoading(isLoading)) @@ -231,22 +199,4 @@ private extension SettingViewModel { } } - private func clearCacheDirectory() throws { - let directory = try FileManager.default.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: false - ) - let imageDir = directory.appendingPathComponent("webPageImages", isDirectory: true) - guard FileManager.default.fileExists(atPath: imageDir.path) else { return } - let contents = try FileManager.default.contentsOfDirectory( - at: imageDir, - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) - for url in contents { - try FileManager.default.removeItem(at: url) - } - } } diff --git a/DevLog/UI/Profile/ProfileView.swift b/DevLog/UI/Profile/ProfileView.swift index 459af9ac..9723059b 100644 --- a/DevLog/UI/Profile/ProfileView.swift +++ b/DevLog/UI/Profile/ProfileView.swift @@ -103,7 +103,9 @@ struct ProfileView: View { signOutUseCase: container.resolve(SignOutUseCase.self), networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self), systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self), - updateSystemThemeUseCase: container.resolve(UpdateSystemThemeUseCase.self) + updateSystemThemeUseCase: container.resolve(UpdateSystemThemeUseCase.self), + fetchWebPageImageDirSizeUseCase: container.resolve(FetchWebPageImageDirSizeUseCase.self), + clearWebPageImageDirectoryUseCase: container.resolve(ClearWebPageImageDirectoryUseCase.self) )) .environment(router) case .activity(let todoId): From 46ba90ec60155a4c654ff15c1c4d6af8a72f4cce Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 16:59:16 +0900 Subject: [PATCH 07/11] =?UTF-8?q?refactor:=20=EC=9B=B9=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=EC=9D=84=20=EC=8A=A4=ED=86=A0=EC=96=B4?= =?UTF-8?q?=EC=99=80=20=EC=9C=A0=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/InfraAssembler.swift | 4 +- .../Service/WebPageMetadataService.swift | 51 ++++--------------- 2 files changed, 13 insertions(+), 42 deletions(-) diff --git a/DevLog/App/Assembler/InfraAssembler.swift b/DevLog/App/Assembler/InfraAssembler.swift index 33fcce01..bd4827bb 100644 --- a/DevLog/App/Assembler/InfraAssembler.swift +++ b/DevLog/App/Assembler/InfraAssembler.swift @@ -53,7 +53,9 @@ final class InfraAssembler: Assembler { } container.register(WebPageMetadataService.self) { - WebPageMetadataService() + WebPageMetadataService( + store: container.resolve(WebPageImageStore.self) + ) } container.register(NWPathConnectivityProvider.self) { diff --git a/DevLog/Infra/Service/WebPageMetadataService.swift b/DevLog/Infra/Service/WebPageMetadataService.swift index d47c29e0..c7dcfa2a 100644 --- a/DevLog/Infra/Service/WebPageMetadataService.swift +++ b/DevLog/Infra/Service/WebPageMetadataService.swift @@ -10,8 +10,13 @@ import LinkPresentation import UIKit final class WebPageMetadataService { + private let imageStore: WebPageImageStore private let logger = Logger(category: "WebPageMetadataService") + init(store: WebPageImageStore) { + self.imageStore = store + } + func fetchMetadata(from urlString: String) async throws -> WebPageMetadataResponse { logger.info("Fetching metadata for URL: \(urlString)") @@ -46,12 +51,7 @@ final class WebPageMetadataService { } do { - let removed = try await Task.detached(priority: .utility) { - let fileURL = try Self.cacheFileURL(for: url) - guard FileManager.default.fileExists(atPath: fileURL.path) else { return false } - try FileManager.default.removeItem(at: fileURL) - return true - }.value + let removed = try imageStore.removeImage(for: url) if removed { logger.info("Removed cached image for URL: \(urlString)") @@ -66,11 +66,12 @@ final class WebPageMetadataService { throw URLError(.badURL) } - return try Self.cacheFileURL(for: url) + return try imageStore.cachedImageURL(for: url) } private func extractImageURL(from imageProvider: NSItemProvider?, url: URL) async throws -> URL? { guard let imageProvider else { return nil } + let imageStore = self.imageStore return try await withCheckedThrowingContinuation { continuation in imageProvider.loadObject(ofClass: UIImage.self) { image, error in @@ -86,44 +87,12 @@ final class WebPageMetadataService { } do { - let fileURL = try Self.cacheFileURL(for: url) - Task.detached { [data, fileURL] in - do { - try data.write(to: fileURL, options: [.atomic]) - continuation.resume(returning: fileURL) - } catch { - continuation.resume(throwing: error) - } - } + let fileURL = try imageStore.saveImage(data, for: url) + continuation.resume(returning: fileURL) } catch { continuation.resume(throwing: error) } } } } - - private static func cacheFileURL(for url: URL) throws -> URL { - let imageDir = try imageDirectoryURL() - let fileName = url.absoluteString - .addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? UUID().uuidString - - return imageDir - .appendingPathComponent(fileName) - .appendingPathExtension("jpeg") - } - - private static func imageDirectoryURL() throws -> URL { - let directory = try FileManager.default.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) - let imageDir = directory.appendingPathComponent("webPageImages", isDirectory: true) - if !FileManager.default.fileExists(atPath: imageDir.path) { - try FileManager.default.createDirectory(at: imageDir, withIntermediateDirectories: true) - } - - return imageDir - } } From 5462b58ea167a883667335f8d820845a8ad05f6f Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 17:08:45 +0900 Subject: [PATCH 08/11] =?UTF-8?q?refactor:=20=EC=9B=B9=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=AA=85=EC=9D=84=20=ED=95=B4=EC=8B=9C=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Storage/Persistence/WebPageImageStore.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/DevLog/Storage/Persistence/WebPageImageStore.swift b/DevLog/Storage/Persistence/WebPageImageStore.swift index df5d9d52..1f75899e 100644 --- a/DevLog/Storage/Persistence/WebPageImageStore.swift +++ b/DevLog/Storage/Persistence/WebPageImageStore.swift @@ -6,6 +6,7 @@ // import Combine +import CryptoKit import Foundation final class WebPageImageStore { @@ -19,8 +20,7 @@ final class WebPageImageStore { func cachedImageURL(for url: URL) throws -> URL { let imageDirectoryURL = try self.imageDirectoryURL(create: true) - let fileName = url.absoluteString - .addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? UUID().uuidString + let fileName = hashedFileName(for: url) return imageDirectoryURL .appendingPathComponent(fileName) @@ -89,6 +89,11 @@ final class WebPageImageStore { } private extension WebPageImageStore { + func hashedFileName(for url: URL) -> String { + let hashValue = SHA256.hash(data: Data(url.absoluteString.utf8)) + return hashValue.map { String(format: "%02x", $0) }.joined() + } + func imageDirectoryURL(create: Bool) throws -> URL { let directory = try fileManager.url( for: .applicationSupportDirectory, From 263354e467cf8eca611c7716d76110cab1788327 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 17:26:05 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=9E=91=EC=97=85=EC=9D=84=20=EB=B0=B1=EA=B7=B8?= =?UTF-8?q?=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EC=8A=A4=EB=A0=88=EB=93=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9E=91=EC=97=85=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WebPageImageRepositoryImpl.swift | 14 ++++++--- .../Protocol/WebPageImageRepository.swift | 4 +-- .../FetchWebPageImageDirSizeUseCase.swift | 2 +- .../FetchWebPageImageDirSizeUseCaseImpl.swift | 4 +-- .../ClearWebPageImageDirectoryUseCase.swift | 2 +- ...learWebPageImageDirectoryUseCaseImpl.swift | 4 +-- .../ViewModel/SettingViewModel.swift | 31 ++++++++++++++----- 7 files changed, 41 insertions(+), 20 deletions(-) diff --git a/DevLog/Data/Repository/WebPageImageRepositoryImpl.swift b/DevLog/Data/Repository/WebPageImageRepositoryImpl.swift index d14d6e1a..df991373 100644 --- a/DevLog/Data/Repository/WebPageImageRepositoryImpl.swift +++ b/DevLog/Data/Repository/WebPageImageRepositoryImpl.swift @@ -12,11 +12,17 @@ final class WebPageImageRepositoryImpl: WebPageImageRepository { self.store = store } - func fetchDirSizeInBytes() -> Int64 { - store.dirSizeInBytes() + func fetchDirSizeInBytes() async -> Int64 { + let store = self.store + return await Task.detached(priority: .utility) { + store.dirSizeInBytes() + }.value } - func clearDirectory() throws { - try store.clearDirectory() + func clearDirectory() async throws { + let store = self.store + try await Task.detached(priority: .utility) { + try store.clearDirectory() + }.value } } diff --git a/DevLog/Domain/Protocol/WebPageImageRepository.swift b/DevLog/Domain/Protocol/WebPageImageRepository.swift index f7a0ee76..d44e0b66 100644 --- a/DevLog/Domain/Protocol/WebPageImageRepository.swift +++ b/DevLog/Domain/Protocol/WebPageImageRepository.swift @@ -6,6 +6,6 @@ // protocol WebPageImageRepository { - func fetchDirSizeInBytes() -> Int64 - func clearDirectory() throws + func fetchDirSizeInBytes() async -> Int64 + func clearDirectory() async throws } diff --git a/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift b/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift index e6807163..cdba5c2f 100644 --- a/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift +++ b/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCase.swift @@ -6,5 +6,5 @@ // protocol FetchWebPageImageDirSizeUseCase { - func execute() -> Int64 + func execute() async -> Int64 } diff --git a/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift b/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift index 56c768cd..513262f5 100644 --- a/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift +++ b/DevLog/Domain/UseCase/WebPage/Fetch/FetchWebPageImageDirSizeUseCaseImpl.swift @@ -12,7 +12,7 @@ final class FetchWebPageImageDirSizeUseCaseImpl: FetchWebPageImageDirSizeUseCase self.repository = repository } - func execute() -> Int64 { - repository.fetchDirSizeInBytes() + func execute() async -> Int64 { + await repository.fetchDirSizeInBytes() } } diff --git a/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift b/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift index 5ede29fb..f2b5702a 100644 --- a/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift +++ b/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCase.swift @@ -6,5 +6,5 @@ // protocol ClearWebPageImageDirectoryUseCase { - func execute() throws + func execute() async throws } diff --git a/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift b/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift index 0f916460..7443208a 100644 --- a/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift +++ b/DevLog/Domain/UseCase/WebPage/Upsert/ClearWebPageImageDirectoryUseCaseImpl.swift @@ -12,7 +12,7 @@ final class ClearWebPageImageDirectoryUseCaseImpl: ClearWebPageImageDirectoryUse self.repository = repository } - func execute() throws { - try repository.clearDirectory() + func execute() async throws { + try await repository.clearDirectory() } } diff --git a/DevLog/Presentation/ViewModel/SettingViewModel.swift b/DevLog/Presentation/ViewModel/SettingViewModel.swift index a9b48c97..c79d3c92 100644 --- a/DevLog/Presentation/ViewModel/SettingViewModel.swift +++ b/DevLog/Presentation/ViewModel/SettingViewModel.swift @@ -24,6 +24,7 @@ final class SettingViewModel: Store { enum Action { case networkStatusChanged(Bool) case setAlert(isPresented: Bool, type: AlertType? = nil) + case setDirSize(Int64) case setLoading(Bool) case setTheme(SystemTheme) case updateDirSize @@ -34,7 +35,9 @@ final class SettingViewModel: Store { } enum SideEffect { + case clearWebPageImageDirectory case deleteAuth + case fetchWebPageImageDirSize case signOut } @@ -86,13 +89,15 @@ final class SettingViewModel: Store { state.isNetworkConnected = isConnected case .setAlert(let isPresented, let type): setAlert(&state, isPresented: isPresented, type: type) + case .setDirSize(let value): + state.dirSize = value case .setLoading(let value): state.isLoading = value case .setTheme(let value): state.theme = value updateSystemThemeUseCase.execute(value) case .updateDirSize: - state.dirSize = fetchWebPageImageDirSizeUseCase.execute() + effects = [.fetchWebPageImageDirSize] case .tapDeleteAuthButton: effects = [.deleteAuth] case .tapSignOutButton: @@ -100,13 +105,8 @@ final class SettingViewModel: Store { case .tapRemoveCacheButton: setAlert(&state, isPresented: true, type: .removeCache) case .confirmRemoveCache: - do { - setAlert(&state, isPresented: false) - try clearWebPageImageDirectoryUseCase.execute() - state.dirSize = fetchWebPageImageDirSizeUseCase.execute() - } catch { - setAlert(&state, isPresented: true, type: .error) - } + setAlert(&state, isPresented: false) + effects = [.clearWebPageImageDirectory] } if self.state != state { self.state = state } @@ -115,6 +115,16 @@ final class SettingViewModel: Store { func run(_ effect: SideEffect) { switch effect { + case .clearWebPageImageDirectory: + Task { + do { + try await clearWebPageImageDirectoryUseCase.execute() + let dirSize = await fetchWebPageImageDirSizeUseCase.execute() + send(.setDirSize(dirSize)) + } catch { + send(.setAlert(isPresented: true, type: .error)) + } + } case .deleteAuth: beginLoading(.delayed) Task { @@ -126,6 +136,11 @@ final class SettingViewModel: Store { send(.setAlert(isPresented: true, type: .error)) } } + case .fetchWebPageImageDirSize: + Task { + let dirSize = await fetchWebPageImageDirSizeUseCase.execute() + send(.setDirSize(dirSize)) + } case .signOut: beginLoading(.delayed) Task { From 04de7ec3648e8a1c5b3ba8e02266b660b53483eb Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 17:35:35 +0900 Subject: [PATCH 10/11] =?UTF-8?q?refactor:=20Application=20Support=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=EC=97=90=EC=84=9C=EC=9D=98?= =?UTF-8?q?=20=EB=B0=B1=EC=97=85=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Storage/Persistence/WebPageImageStore.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DevLog/Storage/Persistence/WebPageImageStore.swift b/DevLog/Storage/Persistence/WebPageImageStore.swift index 1f75899e..b7b1d8fd 100644 --- a/DevLog/Storage/Persistence/WebPageImageStore.swift +++ b/DevLog/Storage/Persistence/WebPageImageStore.swift @@ -105,6 +105,12 @@ private extension WebPageImageStore { 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 } From fc7163b4428993e397fa0122710e033f46e8e1e8 Mon Sep 17 00:00:00 2001 From: opficdev Date: Tue, 14 Apr 2026 17:38:19 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Storage/Persistence/WebPageImageStore.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/DevLog/Storage/Persistence/WebPageImageStore.swift b/DevLog/Storage/Persistence/WebPageImageStore.swift index b7b1d8fd..e89e2d90 100644 --- a/DevLog/Storage/Persistence/WebPageImageStore.swift +++ b/DevLog/Storage/Persistence/WebPageImageStore.swift @@ -61,10 +61,6 @@ final class WebPageImageStore { } } - func observeDirSize() -> AnyPublisher { - subject.eraseToAnyPublisher() - } - func clearDirectory() throws { let imageDirectoryURL = try self.imageDirectoryURL(create: false) guard fileManager.fileExists(atPath: imageDirectoryURL.path) else { return }