Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 78 additions & 5 deletions DevLog/Presentation/ViewModel/SettingViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
final class SettingViewModel: Store {
struct State {
var theme = ""
var dirSize: Int64 = 0
var isLoading = false
var showAlert: Bool = false
var alertTitle: String = ""
Expand All @@ -23,8 +24,11 @@ final class SettingViewModel: Store {
case setAlert(isPresented: Bool, type: AlertType? = nil)
case setLoading(Bool)
case setTheme(String)
case updateDirSize
case tapDeleteAuthButton
case tapSignOutButton
case tapRemoveCacheButton
case confirmRemoveCache
}

enum SideEffect {
Expand All @@ -33,15 +37,14 @@ final class SettingViewModel: Store {
}

enum AlertType {
case signOut, delete, error
case signOut, deleteAuth, error, removeCache
}

@Published private(set) var state = State()
private let deleteAuthuseCase: DeleteAuthUseCase
private let signOutUseCase: SignOutUseCase
private let sessionUseCase: AuthSessionUseCase

@Published private(set) var state = State()

init(
deleteAuthUseCase: DeleteAuthUseCase,
signOutUseCase: SignOutUseCase,
Expand All @@ -60,10 +63,22 @@ final class SettingViewModel: Store {
state.isLoading = value
case .setTheme(let value):
state.theme = value
case .updateDirSize:
state.dirSize = dirSizeInBytes()
case .tapDeleteAuthButton:
return [.deleteAuth]
case .tapSignOutButton:
return [.signOut]
case .tapRemoveCacheButton:
setAlert(&state, isPresented: true, type: .removeCache)
case .confirmRemoveCache:
do {
setAlert(&state, isPresented: false)
try clearCacheDirectory()
state.dirSize = dirSizeInBytes()
} catch {
setAlert(&state, isPresented: true, type: .error)
}
}
return []
}
Expand Down Expand Up @@ -101,23 +116,81 @@ private extension SettingViewModel {
func setAlert(
_ state: inout State,
isPresented: Bool,
type: AlertType?
type: AlertType? = nil
) {
switch type {
case .signOut:
state.alertTitle = "로그아웃"
state.alertMessage = "로그아웃 하시겠습니까?"
case .delete:
case .deleteAuth:
state.alertTitle = "정말 탈퇴하시겠습니까?"
state.alertMessage = "회원 탈퇴가 진행되면 모든 데이터가 지워지고 복구할 수 없습니다."
case .error:
state.alertTitle = "오류"
state.alertMessage = "문제가 발생했습니다. 잠시 후 다시 시도해주세요."
case .removeCache:
state.alertTitle = "임시 데이터 삭제"
state.alertMessage = "임시 데이터를 삭제하고 정리합니다.\n계속하시겠습니까?"
case .none:
state.alertTitle = ""
state.alertMessage = ""
}
state.showAlert = isPresented
state.alertType = type
}

func dirSizeInBytes() -> Int64 {
do {
let cachesDir = try FileManager.default.url(
for: .cachesDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
)
guard FileManager.default.fileExists(atPath: cachesDir.path) else { return 0 }
return directorySize(at: cachesDir)
} catch {
return 0
}
Comment on lines +142 to +154
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

dirSizeInBytes() 함수에서 FileManager.default.url 호출 시 create: true로 설정되어 있습니다. 캐시 디렉토리가 존재하지 않을 경우 생성하는 것은 좋지만, 단순히 크기를 측정하는 함수에서 디렉토리를 생성하는 부수 효과를 가지는 것은 함수의 단일 책임 원칙에 위배될 수 있습니다. 디렉토리 생성은 캐시를 사용하기 전에 한 번만 수행하는 것이 더 적절합니다.

}

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
Comment on lines +157 to +176
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

directorySize(at:) 함수에서 FileManager.default.enumerator를 사용하여 디렉토리 크기를 계산하고 있습니다. 이 방법은 큰 디렉토리의 경우 성능 문제가 발생할 수 있습니다. URL.volumeResourceValues(forKeys:)를 사용하여 volumeAvailableCapacityForImportantUsageKey 또는 volumeTotalCapacityKey를 확인하는 것이 더 효율적일 수 있습니다. 다만, 이는 전체 볼륨의 용량을 가져오므로, 특정 디렉토리의 정확한 크기를 얻기 위해서는 현재 방식이 필요할 수 있습니다. 하지만 FileManager.default.contentsOfDirectory(at:includingPropertiesForKeys:options:)를 사용하여 각 파일의 fileSize를 합산하는 방식이 더 일반적이고 효율적일 수 있습니다.

}

private func clearCacheDirectory() throws {
let cachesDir = try FileManager.default.url(
for: .cachesDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
)
guard FileManager.default.fileExists(atPath: cachesDir.path) else { return }
let contents = try FileManager.default.contentsOfDirectory(
at: cachesDir,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
)
for url in contents {
try FileManager.default.removeItem(at: url)
}
}
Comment on lines +179 to +195
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

clearCacheDirectory() 함수에서 FileManager.default.contentsOfDirectory를 사용하여 디렉토리 내용을 가져온 후 각 항목을 개별적으로 삭제하고 있습니다. 캐시 디렉토리 전체를 삭제하려면 FileManager.default.removeItem(at: cachesDir)를 사용하는 것이 더 간단하고 효율적입니다. 이 경우 cachesDir 자체를 삭제하고 필요하다면 다시 생성하는 방식이 될 것입니다.

    private func clearCacheDirectory() throws {
        let cachesDir = try FileManager.default.url(
            for: .cachesDirectory,
            in: .userDomainMask,
            appropriateFor: nil,
            create: true
        )
        try FileManager.default.removeItem(at: cachesDir)
        // 필요하다면 디렉토리를 다시 생성할 수 있습니다.
        // try FileManager.default.createDirectory(at: cachesDir, withIntermediateDirectories: true, attributes: nil)
    }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

캐시 디렉토리는 시스템이 관리하므로 제거할 수 없음

}
3 changes: 3 additions & 0 deletions DevLog/Resource/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@
},
"읽지 않음" : {

},
"임시 데이터 삭제" : {

},
"작년" : {

Expand Down
51 changes: 44 additions & 7 deletions DevLog/UI/Setting/SettingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ struct SettingView: View {
@Environment(\.diContainer) var container: DIContainer
@StateObject var viewModel: SettingViewModel
@EnvironmentObject var router: NavigationRouter
@State private var navigationPath: Path?

var body: some View {
Form {
Expand All @@ -28,16 +27,27 @@ struct SettingView: View {
.foregroundStyle(Color.gray)
}
}
.onAppear {
viewModel.send(.setTheme(theme.localizedName))
}

Button {
router.push(Path.pushNotification)
} label: {
Text("알림")
.foregroundStyle(Color.primary)
}

let dirSize = viewModel.state.dirSize
Button {
viewModel.send(.tapRemoveCacheButton)
} label: {
HStack {
Text("임시 데이터 삭제")
.foregroundStyle(dirSize == 0 ? Color.secondary : .primary)
Spacer()
Text(formatFileSize(bytes: dirSize))
.foregroundStyle(Color.secondary.opacity(dirSize == 0 ? 0 : 1))
}
}
.disabled(dirSize == 0)
Comment on lines +38 to +50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

dirSize == 0일 때 버튼을 disabled 처리하는 로직은 좋습니다. 하지만 foregroundStyle에서 Color.secondary.opacity(dirSize == 0 ? 0 : 1)를 사용하는 대신, dirSize == 0일 때 Color.secondary를 사용하고 그렇지 않을 때 Color.primary를 사용하는 것이 더 명확하고 일관된 UI를 제공할 수 있습니다. 현재 opacity(0)는 텍스트를 완전히 숨기므로, 사용자가 캐시가 없다는 것을 인지하기 어려울 수 있습니다.

Suggested change
let dirSize = viewModel.state.dirSize
Button {
viewModel.send(.tapRemoveCacheButton)
} label: {
HStack {
Text("임시 데이터 삭제")
.foregroundStyle(dirSize == 0 ? Color.secondary : .primary)
Spacer()
Text(formatFileSize(bytes: dirSize))
.foregroundStyle(Color.secondary.opacity(dirSize == 0 ? 0 : 1))
}
}
.disabled(dirSize == 0)
let dirSize = viewModel.state.dirSize
Button {
viewModel.send(.tapRemoveCacheButton)
} label: {
HStack {
Text("임시 데이터 삭제")
.foregroundStyle(dirSize == 0 ? Color.secondary : .primary)
Spacer()
Text(formatFileSize(bytes: dirSize))
.foregroundStyle(dirSize == 0 ? Color.secondary : .primary)
}
}
.disabled(dirSize == 0)

}

Section {
Expand Down Expand Up @@ -79,7 +89,6 @@ struct SettingView: View {
Section {
Button {
router.push(Path.account)

} label: {
Text("계정 연동")
}
Expand All @@ -93,7 +102,7 @@ struct SettingView: View {
HStack {
Spacer()
Button(role: .destructive, action: {
viewModel.send(.setAlert(isPresented: true, type: .delete))
viewModel.send(.setAlert(isPresented: true, type: .deleteAuth))
}) {
Text("회원 탈퇴")
.font(.headline)
Expand Down Expand Up @@ -139,6 +148,10 @@ struct SettingView: View {
LoadingView()
}
}
.onAppear {
viewModel.send(.setTheme(theme.localizedName))
viewModel.send(.updateDirSize)
}
}

private enum Path: Hashable {
Expand All @@ -155,17 +168,41 @@ struct SettingView: View {
Button("확인", role: .destructive) {
viewModel.send(.tapSignOutButton)
}
case .delete:
case .deleteAuth:
Button("취소", role: .cancel) {
viewModel.send(.setAlert(isPresented: false))
}
Button("탈퇴", role: .destructive) {
viewModel.send(.tapDeleteAuthButton)
}
case .removeCache:
Button("취소", role: .cancel) {
viewModel.send(.setAlert(isPresented: false))
}
Button("확인", role: .destructive) {
viewModel.send(.confirmRemoveCache)
}
case .error, .none:
Button("확인", role: .cancel) {
viewModel.send(.setAlert(isPresented: false))
}
}
}

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])"
}
Comment on lines +192 to +207
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

formatFileSize(bytes:) 함수에서 value /= 1024.0로 나누는 부분은 정확하지만, while 1000 <= value 조건은 1024 <= value로 변경하는 것이 일반적인 파일 크기 단위 변환에 더 적합합니다. 1000 대신 1024를 사용하는 것이 컴퓨터 과학에서 더 표준적인 방식입니다.

Suggested change
private func formatFileSize(bytes: Int64) -> String {
let units = ["B", "KB", "MB", "GB"]
var value = Double(max(bytes, 0))
var unitIndex = 0
while 1000 <= 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])"
}
private func formatFileSize(bytes: Int64) -> String {
let units = ["B", "KB", "MB", "GB"]
var value = Double(max(bytes, 0))
var unitIndex = 0
while 1024 <= 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])"
}

}