Feature/#90 - DeVault Settings 화면 및 설정 기능 구현 - #101
Conversation
- SettingsRepository/Impl에 General/Security/Notifications 카테고리 설정 키 전부 추가 (로그인 시 자동 실행, 기본 환경, 인증 요구, 자동 잠금, 클립보드 자동 비우기, 화면 녹화 차단, 만료 알림, 반복 실패/비정상 접근 알림) - UserDefaultsKey에 Bool 외 Int/String/[Int] 저장 헬퍼 추가, 기본값은 register(defaults:)로 선언 - 카테고리별 UseCase(General/Security/Notification)를 ICloudSyncSettingsUseCase와 동일한 얇은 패턴으로 추가 - FakeSettingsRepository + UseCase 테스트 추가
- 별도 Window 대신 MainFeature.State.settings optional로 MainView 콘텐츠를 스위칭 (createSecret/selectSecretType과 동일한 패턴) - SidebarFeature에 didTapSettings 액션 + settingsTapped delegate 추가, 기존 openWindow(id: "settings") 호출 제거 - SettingsFeature/SettingsView 신규: 카테고리 사이드바(2-column NavigationSplitView) + 7개 카테고리 placeholder 화면(General/Security/iCloud/Notifications/Shortcuts/Data/About) - DevaultApp의 placeholder settingsWindow Scene 제거 - MainFeatureTests에 settings 열기/닫기 테스트 추가
- SettingsClient(@DependencyClient) 신규 — 카테고리별 메서드를 계속 추가해 나갈 단일 Client - 로그인 시 자동 실행: SMAppService.mainApp register/unregister, 실패 시 토글 되돌림 - 기본 환경 드롭다운: 기존 SecretEnvironment enum 재사용, DVDropdown 컴포넌트 사용 - SettingsToggleRow/SettingsSection/SettingsScrollContainer 공용 컴포넌트 추가 (Security/Notifications 등 다른 카테고리에서도 재사용 예정) - GeneralSettingsFeature 테스트 추가
- AppFeature.task: 앱 실행 시 인증 요구 설정이 꺼져 있으면 잠금 없이 바로 main으로 진입 - RevealSecretPayloadUseCaseImpl: 복사 시 인증 요구 설정이 꺼져 있으면 authenticate 생략 (SecuritySettingsUseCase 주입, SecretClient+Live에서 LiveRepositories.settings로 wiring) - SettingsClient에 Security 카테고리 메서드 추가 - SecuritySettingsFeature/View 신규 — 두 토글 UI - AppFeatureTests/RevealSecretPayloadUseCaseImplTests/SecuritySettingsFeatureTests 테스트 추가
- IdleMonitorClient 신규 — CGEventSource.secondsSinceLastEventType 5초 폴링으로 유휴 시간을 스트리밍 - AppFeature.task에서 유휴 스트림을 구독, 설정된 autoLockMinutes를 넘으면 idleTimeoutReached 발송 → main에서 locked로 전환 (0분은 "사용 안 함") 기존 main(.delegate(.lockRequested)) 전환과 동일한 경로 재사용 - SecuritySettingsView에 자동 잠금 시간 드롭다운(1/3/5/15/30분/사용 안 함) 추가 - AppFeatureTests/SecuritySettingsFeatureTests에 관련 테스트 추가
- CopySensitiveValueUseCaseImpl.clipboardClearDelay를 Duration 고정값에서 @sendable () -> Duration? 클로저로 변경 — 매 호출마다 설정을 다시 읽고, nil이면 자동 정리를 사용하지 않음 (AbnormalAccessMonitor 유지를 위해 인스턴스는 그대로 공유) - LiveUseCases.copySensitiveValue 공유 인스턴스 추가 (authenticate와 동일한 이유로 공유 필요) - ClipboardClient(@DependencyClient) 신규, LiveUseCases.copySensitiveValue로 wiring - DetailReadOnlyFieldView의 복사 버튼이 NSPasteboard 직접 호출 대신 ClipboardClient 경유하도록 변경 — 모든 Secret 타입의 복사 가능 필드에 자동 정리·반복 복사 감지 적용 - SecuritySettingsView에 클립보드 자동 비우기 토글 + 시간 드롭다운(15초/30초/1분/5분) 추가
- WindowCaptureBlocker가 설정(isHideDuringScreenRecordingEnabled)에 따라 NSWindow.sharingType을 .none/.readOnly로 전환 - Settings 화면이 이 뷰의 상위 계층이 아니라 SwiftUI 재평가로는 반영이 안 되므로, UserDefaults.didChangeNotification을 직접 구독해 설정 변경을 즉시 반영 - SecuritySettingsView에 화면 녹화 중 값 숨김 토글 추가
- makeICloudSyncUnavailableAlert를 OnboardingFeature private에서 DVPresentation/Sources/Support/ICloudSyncAlert.swift 공유 위치로 추출 (UserAuthenticationAlert.swift와 동일한 <Alert: Equatable> 제네릭 패턴). OnboardingFeature는 makeICloudSyncFailedAlert로 얇게 감싸 재사용 - SettingsRepository/ICloudSyncSettingsUseCase에 iCloudLastSyncedAt 추가 - ICloudSyncStatusClient 신규 — NSPersistentStoreRemoteChange 구독으로 마지막 동기화 시각 갱신, 로컬 count로 동기화된 시크릿/프로젝트 개수 표시, 순수 읽기 전용 계정 상태 확인 (OnboardingClient.enableICloudSync과 달리 설정을 켜는 부작용 없음) - ICloudSettingsFeature/View: 토글(끄기는 즉시 저장, 켜기는 계정 상태 확인 후 실패 시 공유 alert), 상태 카드(연결 상태/마지막 동기화/개수), Sync Now(카운트 갱신), 재시작 필요 안내 배너 (런타임 hot-swap은 범위 밖 — 토글은 다음 재시작부터 반영) - ICloudSettingsFeatureTests 추가
- ScheduleSecretExpiryNotificationsUseCaseImpl의 하드코딩 daysBeforeExpiry([7,3])를 설정 기반 클로저로 변경, 기본값을 명세 숫자(30/7/1/당일)로 갱신. cancel()은 설정과 무관하게 "가능했던 모든" daysBefore를 취소해 설정이 바뀌어도 예전 마크가 안 남도록 함 - 만료 알림 자체 on/off 게이트 추가(꺼지면 취소만 하고 새로 예약 안 함) - AuthenticateUseCaseImpl/CopySensitiveValueUseCaseImpl에 알림 on/off 게이트 추가 (카운팅 자체는 꺼져 있어도 계속 — 나중에 다시 켰을 때 그 사이 실패가 없었던 것처럼 안 되도록) - LiveUseCases.expirySchedule 공유 인스턴스 추가 — 3곳에서 중복 작성하던 설정 읽기 클로저를 통합 - SettingsClient에 Notifications 카테고리 메서드 추가, macOS 알림 권한 상태 확인 + 시스템 설정 딥링크(x-apple.systempreferences:com.apple.preference.notifications, 실기 검증됨) - NotificationSettingsFeature/View: 만료 알림 on/off + 타이밍 다중 선택(DVMultiSelectDropdown), 반복 인증 실패/클립보드 비정상 접근 알림 토글, 알림 권한 꺼짐 배너
- 전역 단축키(Quick Launcher/Lock Now)는 1차 배포에서 제외하기로 확정 — 별도 인프라(Input Monitoring 권한, 레코더 UI, 충돌 감지) 필요 - 명세의 검색(⌘F)·값 복사(⌘C)는 대응하는 기능(검색 UI, 단일 복사 대상)이 없어 표에서 제외 — 동작하지 않는 단축키를 있는 것처럼 보여주지 않기 위함 - 대신 새 시크릿(⌘N), 설정 열기(⌘,) 두 개를 사이드바 버튼에 실제로 연결하고 정적 표로 표시
- DeleteAllDataUseCase 신규: .all/.deleted 컬렉션 모두 조회해 Secret 전량 + Project 전량을 hard delete로 영구 삭제 - 실행 전 AuthenticateUseCase로 재인증(Touch ID/시스템 암호) 요구, 인증 실패 시 삭제 전혀 진행 안 함 - 순차 삭제이며 원자적 배치 삭제는 아님(1차 배포 범위로 충분하다고 판단) - DataSettingsFeature: 삭제 버튼 → 확인 alert → 재인증+삭제 실행 → 성공/실패 결과를 alert로 안내 - 클립보드 백그라운드 상시 감시 토글은 1차 배포 제외 결정에 따라 미포함
- Bundle.main.infoDictionary에서 버전/빌드 번호를 읽어 표시 - 개발자 크레딧은 팀명(Devault Team)만 표시 - MIT 라이선스는 GitHub 저장소 LICENSE 파일로 링크 - 정적 콘텐츠라 별도 Feature/UseCase 없이 View만으로 구현
- iCloud 상태 카드/알림 권한 배너 아이콘에 accessibilityHidden 적용(인접 텍스트와 의미 중복이라 온보딩과 동일 기준 적용) - SettingsFeature(카테고리 전환, 닫기 델리게이트)에 대한 테스트 신규 작성
allPossibleDaysBeforeExpiry가 private라 init 기본 인자식(호출부 컨텍스트에서 평가됨)에서 접근 불가능했다. 같은 파일 내부에서만 쓰이므로 fileprivate로 완화.
기본 인자 표현식은 호출부 컨텍스트에서 평가되므로 private/fileprivate 모두 접근 불가. 같은 모듈 내부에서만 쓰이는 상수라 internal(기본 접근 수준)이면 충분.
기본 환경과 자동 잠금 사용 여부를 명시적으로 저장한다. 화면 캡처 차단 설정 변경 스트림을 Repository에 추가한다.
공용 SettingsClient를 제거하고 각 설정 화면에 필요한 인터페이스와 Live 구현을 분리한다.
상세 화면을 중앙 정렬된 단일 스크롤 Form으로 구성한다. Toggle, Picker, Button, Link, Value Row를 공용 컴포넌트로 제공한다.
실제 로그인 항목 상태를 Service와 Repository에 동기화한다. 기본 환경은 Development를 초기값으로 사용하고 세 가지 환경만 제공한다.
자동 잠금과 클립보드 비우기를 사용 여부와 시간 선택으로 분리한다. 선택 가능한 시간 값은 화면 모델로 관리한다.
알림 시점을 다중 선택 가능한 값 모델로 제공한다. 모든 만료 알림은 선택한 날짜의 오전 9시에 예약하고 당일 문구를 구분한다.
앱 버전만 표시하고 라이선스와 GitHub 링크를 전용 Row로 구성한다.
단축키 정보를 공용 Settings Form과 읽기 전용 Row로 표시한다.
인증, 복사, Secret 공개 유스케이스가 SettingsRepository에서 정책을 직접 조회한다. 중복 ClipboardClient와 Composition 클로저를 제거한다.
현재 ModelContainer의 Vault 데이터를 한 번의 저장으로 삭제하는 Repository를 추가한다. Data 설정은 재인증 후 초기화를 수행하고 iCloud 연동 영향을 안내한다.
iCloud 계정 확인과 원격 변경 감지를 ICloudService로 통합한다. 온보딩과 설정 변경 시 현재 Repository 묶음을 새 ModelContainer로 교체한다.
앱과의 마지막 상호작용 시간을 Service에서 측정한다. 자동 잠금 설정을 UseCase에서 해석해 타임아웃 스트림으로 제공한다.
WindowCaptureBlocker의 직접 의존성 조회를 제거한다. App 상태로 전달된 설정값에 따라 NSWindow 공유 정책을 갱신한다.
메인 세션에서 비활성 타임아웃과 캡처 차단 설정을 구독한다. iCloud 원격 변경 시 목록과 만료 알림을 갱신한다.
강제 동기화처럼 보이던 개수 조회를 계정 상태 새로고침으로 대체한다. 원격 변경 감지 시각을 표시하고 동기화 해제 전 데이터 보존 정책을 안내한다.
만료 알림 활성화와 발송 시점 변경 후 전체 예약을 바로 다시 동기화한다. 재예약 실패 시 저장 결과와 재시도 안내를 표시한다.
전체 데이터 삭제와 iCloud 저장소 전환 성공을 Main까지 전달한다. 이전 Vault 화면 상태를 초기화하고 Secret 목록과 Sidebar를 다시 조회한다.
값 일부 숨김으로 보이던 설정을 앱 창 전체 캡처 보호로 명확히 한다. 레이어별 API 이름을 통일하고 기존 UserDefaults 값은 유지한다.
더 이상 사용하지 않는 Settings 문자열을 제거한다. iCloud 상태, 알림 재예약 실패와 창 캡처 보호 문구를 현재 UI에 맞춰 반영한다.
존재하지 않는 debug action 분기를 제거하고 DetailReadOnlyFieldView extension의 닫는 중괄호를 복구한다.
Reveal은 복사 설정과 무관하게 항상 인증하도록 정리한다. CopySensitiveValueUseCase는 복사 인증 설정을 확인해 필요할 때 인증한 뒤 클립보드에 기록하며, 미복호화 Copy 경로의 중복 인증 가능성은 TODO로 남긴다.
|
Important Review skippedToo many files! This PR contains 135 files, which is 35 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (135)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
doyeonk429
left a comment
There was a problem hiding this comment.
PR #101 코드 리뷰입니다. Domain → Data → Composition → Presentation 순으로 전체를 확인했고, 아래 8건을 인라인으로 남겼습니다. 중요도순입니다.
| # | 심각도 | 내용 |
|---|---|---|
| 1 | Critical | 첫 복사 시 Touch ID 2회 (기본 설정 경로) |
| 2 | Critical | 자동 잠금이 시스템 슬립 구간을 건너뜀 |
| 3 | High | 로그인 항목 토글이 .requiresApproval에서 조용히 되돌아감 |
| 4 | High | "Delete All Data" 후 Keychain 마스터 키·BackupRecord 잔존 |
| 5 | Medium | 알림 취소 목록 하드코딩 → 옵션 추가 시 고아 알림 |
| 6 | Medium | "expire in 1 days" 복수형 미처리 |
| 7 | Medium | 1초 폴링 루프가 앱 수명 내내 MainActor에서 동작 |
| 8 | Medium | 전체 삭제 실패 원인 에러 유실 |
잘 된 부분도 남깁니다.
LiveStorage를 actor로 두고RepositorySet을 원자적으로 교체하되, 새 묶음 생성이 성공했을 때만current를 바꾸는 설계가 견고합니다. 실패 시setICloudSyncEnabled도 호출되지 않아 설정과 실제 저장소가 어긋나지 않습니다.LocalStorage.makeDefault가 두 모드 모두 같은"Synced"저장소를 쓰고 CloudKit mirroring만 토글한다는 점 — 데이터 유실 없는 hot swap의 핵심인데 주석으로 정확히 남겨두셨습니다.- 저장소 지연 초기화로 "온보딩 전에 ModelContainer가 고정된다"는 기존 TODO를 실제로 해소했습니다.
makeICloudSyncUnavailableAlert를ICloudSyncAlert.swift로 추출해 온보딩/Settings가 공유하게 한 것, 새ICloudAccountStatus케이스 추가 시 컴파일러가 강제하도록 exhaustive switch를 유지한 주석까지 좋습니다.AuthenticateUseCaseImpl에서abnormalAccessMonitor.recordAccess(...)를 설정 확인보다 먼저 평가해, 알림이 꺼져 있어도 카운터는 유지되도록 한 단락 평가 순서가 정확합니다.- 만료 알림 오전 9시 계산은
Calendar.date(bySettingHour:)의 전진 탐색 동작이 의심돼서 따로 실행 검증했는데, 같은 날 09:00을 정확히 반환합니다 — 문제 없습니다.
프로세스 관련 한 가지. 129 파일 / +6,584 / 커밋 37개는 한 PR로 리뷰하기 부담스러운 크기입니다. 특히 iCloud 저장소 hot swap과 전체 데이터 초기화는 "Settings 화면 구현"과 성격이 다른 인프라 변경이라, 다음부터 분리해주시면 리뷰 품질이 올라갈 것 같습니다. Localizable.xcstrings는 추가만 있고 삭제된 키가 없어 깨끗합니다.
|
|
||
| public func execute(_ value: String) async throws { | ||
| if settingsRepository.isRequireAuthToCopyEnabled() { | ||
| try await authenticateUseCase.authenticate(reason: AuthenticationReason.copySecret) |
There was a problem hiding this comment.
[Critical 1/8] 첫 복사 시 Touch ID가 2번 뜹니다 (기본 설정 경로)
SecretDetailFeature에서 payload가 아직 없을 때 복사를 누르면 이 경로를 탑니다:
didTapCopy (SecretDetailFeature.swift:255)
└ revealEffect(thenCopy: field)
└ RevealSecretPayloadUseCaseImpl → authenticate(reason: .revealSecret) ← 1회
└ payloadResponse(.success)
└ copyEffect
└ CopySensitiveValueUseCaseImpl.execute
└ authenticate(reason: .copySecret) ← 2회
isRequireAuthToCopyEnabled는 SettingsRepositoryImpl의 register(defaults:)에서 기본 true라, "시크릿을 열고 바로 복사"라는 가장 흔한 동선에서 인증 프롬프트가 연속 두 번 뜹니다. SecretDetailFeature.swift:249-251의 TODO로 인지는 되어 있지만 이 상태로 릴리스되면 기본 사용감이 많이 상합니다.
제안:
payloadResponse에서 이어지는 copy는 방금 끝난 reveal 인증을 재사용하도록(인증 없는 복사 경로 사용) 처리하거나isRequireAuthToCopyEnabled기본값을false로 두거나
둘 중 하나는 이번 PR에서 정리하면 좋겠습니다.
덧붙여, 이건 기존 정책의 반전입니다. develop의 SecretClient.copySensitiveValue 문서 주석이 **인증하지 않는다** — 복사 시 인증은 정책상 요구하지 않는다 였습니다. 정책 변경 자체는 괜찮지만 PR 본문에 명시해주시면 좋겠습니다.
| func inactivitySeconds() -> TimeInterval { | ||
| lock.lock() | ||
| defer { lock.unlock() } | ||
| return ProcessInfo.processInfo.systemUptime - lastInteractionUptime |
There was a problem hiding this comment.
[Critical 2/8] 자동 잠금이 시스템 슬립 구간을 건너뜁니다
ProcessInfo.processInfo.systemUptime은 정의상 "마지막 재시동 이후 시스템이 깨어 있던 시간" 이라 슬립 구간을 세지 않습니다.
재현 시나리오:
- 시크릿을 보다가 마우스를 움직인 직후 노트북 덮개를 닫음
- 8시간 뒤 덮개를 엶
systemUptime - lastInteractionUptime≈ 0초 → 자동 잠금이 걸리지 않고 금고가 열린 채로 나타남
시크릿 매니저의 자동 잠금에서 가장 방어해야 할 상황이라 생각합니다.
제안:
- wall-clock을 병행해서
max(uptimeDelta, wallClockDelta)로 판단 (Date()단독은 시스템 시계 변경에 취약하므로 둘 다 두는 편이 안전합니다) - 또는
NSWorkspace.didWakeNotification/screensDidSleepNotification에서 잠금 체크를 강제
관련해서 화면 보호기/세션 전환(sessionDidResignActive) 시 잠금도 지금은 없습니다. 이번 범위 밖이면 이슈로만 남겨두셔도 될 것 같습니다.
| public init() {} | ||
|
|
||
| public func isEnabled() -> Bool { | ||
| SMAppService.mainApp.status == .enabled |
There was a problem hiding this comment.
[High 3/8] 로그인 항목 토글이 아무 안내 없이 되돌아갑니다
SMAppService.mainApp.register()는 사용자가 시스템 설정에서 아직 승인하지 않은 상태에서도 성공을 반환하면서 status를 .enabled가 아닌 .requiresApproval로 둡니다.
그래서 이렇게 됩니다:
- 사용자가 토글 ON →
setLaunchAtLoginEnabled(true)성공 →repository에true저장 - 실제
status는.requiresApproval - 다음에 General 탭에 다시 들어오면
.task→GeneralSettingsUseCaseImpl.isLaunchAtLoginEnabled()→isEnabled() == false - 토글이 저절로 꺼져 있고, 왜 그런지는 어디에도 안 뜸
macOS 13+ 에서 꽤 흔하게 밟는 경로입니다. .requiresApproval을 별도 상태로 올려서 "시스템 설정 > 로그인 항목에서 승인이 필요합니다" 안내 + 시스템 설정 딥링크를 붙이는 걸 제안합니다 (Notifications 탭의 permissionBanner와 같은 패턴을 재사용할 수 있어 보입니다).
| for model in projects { modelContext.delete(model) } | ||
| for model in appAuditLogs { modelContext.delete(model) } | ||
|
|
||
| try modelContext.save() |
There was a problem hiding this comment.
[High 4/8] "Delete All Data" 후에도 Keychain 마스터 키와 BackupRecord가 남습니다
alert 문구는 This will permanently delete all secrets and projects. This action cannot be undone. 인데 실제로 남는 게 두 가지 있습니다.
1) Keychain 마스터 키
SecretCryptoServiceImpl은 모든 시크릿이 공유하는 단일 키(com.devault.masterKey.v1)를 Keychain에 둡니다. 리포 전체를 grep 해봤는데 SecItemDelete가 한 군데도 없어서 초기화 후에도 키가 그대로 남습니다.
2) BackupRecord
여기서 삭제 대상에 빠져 있고, DataResetRepositoryImplTests가 #expect(backupRecordCount == 1)로 그 동작을 고정하고 있습니다. 백업 파일 경로와 keyTag를 담은 레코드가 살아남습니다.
둘 다 의도된 선택일 수 있습니다 — 백업은 "시크릿/프로젝트"가 아니고, 키를 남기면 재사용이 되니까요. 다만 지금은 왜 남기는지 설명하는 주석이 어디에도 없습니다. 테스트만 단정문으로 박혀 있어서 나중에 보는 사람은 버그인지 의도인지 알 수 없습니다.
deleteAll()에 BackupRecord를 제외하는 이유를 WHY 주석으로- 마스터 키 폐기/재생성 여부는 명시적으로 결정하고 기록
정도만 부탁드립니다.
|
|
||
| /// 지금까지 설정 화면이 제공한 모든 가능한 옵션 — 설정이 바뀌어도 예전에 예약된 마크를 | ||
| /// 확실히 취소하기 위해 schedule()이 아니라 cancel()에서만 이 고정 목록을 쓴다. | ||
| private static let allPossibleDaysBeforeExpiry = [30, 7, 1, 0] |
There was a problem hiding this comment.
[Medium 5/8] 취소 목록이 하드코딩이라 옵션 추가 시 "취소 불가능한 고아 알림"이 생깁니다
schedule()은 settingsRepository.expiryAlertDaysBefore()(동적)를, cancel()은 이 배열(고정)을 씁니다. 위 주석에 의도가 잘 적혀 있고 그 선택 자체는 맞다고 봅니다.
문제는 ExpiryAlertDay에 새 케이스를 추가하면서 이 배열을 안 고치는 경우입니다. 예를 들어 case fourteenDaysBefore = 14를 추가하면:
schedule()은...-14didentifier로 알림을 예약cancel()은[30, 7, 1, 0]만 취소 →-14d알림은 영원히 안 지워짐- 시크릿을 삭제해도, 만료일을 바꿔도 예전 알림이 계속 발송됨
두 곳이 물리적으로 떨어져 있어서(Presentation의 ExpiryAlertDay ↔ Domain의 이 배열) 실수하기 매우 쉬운 구조입니다.
제안:
- Domain에 만료 알림 시점 목록을 소유하는 타입을 두고
ExpiryAlertDay가 그걸 파생하도록 (방향상 Presentation → Domain 참조가 자연스러움) - 최소한 양쪽에 상호 참조 주석 + 아래 같은 가드 테스트
#expect(
Set(ScheduleSecretExpiryNotificationsUseCaseImpl.allPossibleDaysBeforeExpiry)
.isSuperset(of: Set(ExpiryAlertDay.allCases.map(\.rawValue)))
)참고로 같은 [30, 7, 1, 0] 리터럴이 지금 5곳에 중복돼 있습니다 — SettingsRepositoryImpl.register(defaults:), SettingsRepositoryImpl.expiryAlertDaysBefore()의 ?? fallback, 여기, NotificationSettingsClient 기본값, ExpiryAlertDay.
| ) | ||
|
|
||
| case .secretExpiresSoon(_, let daysBefore): | ||
| if daysBefore == 0 { |
There was a problem hiding this comment.
[Medium 6/8] "A saved secret will expire in 1 days." — 복수형 처리 누락
daysBefore == 0은 전용 문구로 잘 분기하셨는데, 이번 PR에서 expiryAlertDaysBefore 기본값에 1이 새로 들어왔습니다 (기존 lead days는 [7, 3]이라 1은 도달 불가능했습니다).
Localizable.xcstrings를 확인해보니 해당 키에 plural variation이 없습니다:
'A saved secret will expire in %lld days.'
-> {"comment": "The number of days before the secret expires is dynamically inserted here.", ...}
# variations 없음
그래서 만료 1일 전 알림 본문이 "A saved secret will expire in 1 days." 로 나갑니다.
SecurityNotificationModuleTests도 daysBefore 0과 7만 검증하고 있어서 잡히지 않았습니다.
제안: xcstrings에서 해당 키에 plural variation(one / other)을 추가하거나, 0처럼 1도 전용 문구로 분기. 어느 쪽이든 daysBefore: 1 테스트 케이스를 함께 추가해주세요.
| continuation.yield(timestamp.inactivitySeconds()) | ||
|
|
||
| do { | ||
| try await Task.sleep(for: .seconds(1)) |
There was a problem hiding this comment.
[Medium 7/8] 1초 폴링 루프가 앱 수명 내내 MainActor에서 돕니다
이 while + Task.sleep(1s) 루프는 앱이 살아 있는 동안 매초 깨어나고, 매번 continuation.yield → MonitorAppInactivityUseCaseImpl이 UserDefaults를 두 번 읽습니다 (isAutoLockEnabled(), autoLockMinutes()). 자동 잠금 설정이 꺼져 있어도 계속 돕니다.
@MainActor 컨텍스트라 App Nap을 방해하고 배터리에도 불리합니다.
이벤트 모니터가 이미 마지막 상호작용 시각을 정확히 들고 있으니, 폴링 대신
- 마지막 상호작용 기준으로 "타임아웃까지 남은 시간"만큼 한 번 sleep
- 상호작용이 들어오면 그 sleep을 취소하고 다시 계산
하는 구조로 바꾸면 idle 상태에서 타이머가 아예 돌지 않습니다. (자동 잠금 OFF면 스트림을 아예 시작하지 않는 것도 함께 고려해볼 만합니다.)
| try modelContext.save() | ||
| } catch { | ||
| modelContext.rollback() | ||
| throw DataResetRepositoryError.resetFailed |
There was a problem hiding this comment.
[Medium 8/8] 전체 삭제 실패 원인이 통째로 사라집니다
} catch {
modelContext.rollback()
throw DataResetRepositoryError.resetFailed // 원본 error 유실
}전체 데이터 초기화 실패는 사용자가 재시도해도 안 풀릴 수 있는 상황(스키마/CloudKit/디스크)인데, 원인이 로그에도 안 남아서 사후 진단이 불가능합니다. UI에는 Please try again.만 뜹니다.
같은 파일 계층의 다른 곳들처럼 Log.error(...) 한 줄만이라도 남겨주세요.
} catch {
modelContext.rollback()
Log.error("전체 데이터 초기화 실패: \(error)", category: .storage)
throw DataResetRepositoryError.resetFailed
}Reveal 인증과 Copy용 payload 복호화 경로를 분리한다. 미복호화 상태에서 복사해도 CopySensitiveValueUseCase의 설정 기반 인증만 한 번 수행하도록 연결한다.
1초 폴링을 제거하고 앱 상호작용마다 단일 타이머를 재예약한다. ContinuousClock으로 시스템 슬립 시간까지 포함하고 설정 변경 시 타이머를 즉시 갱신한다.
SMAppService의 등록 상태를 Domain으로 올리고 requiresApproval을 등록 의도로 유지한다. 승인 대기 시 안내와 시스템 로그인 항목 설정 이동 버튼을 제공한다.
전체 삭제에서 BackupRecord와 Keychain 마스터 키를 유지하는 이유를 계약과 구현에 문서화한다. reset 실패 원본 오류는 storage 로그에 남긴다.
만료 알림 시점을 Domain ExpiryAlertDay로 통합하고 1일 전 옵션을 3일 전으로 변경한다. 예약과 취소 ID를 동일한 Domain 옵션 목록에서 생성해 고아 알림을 방지한다.
✨ What’s this PR?
📌 관련 이슈 (Related Issue)
🧶 주요 변경 내용 (Summary)
NavigationSplitView기반 Settings 사이드바 및 중앙 정렬 Detail 화면 구현Form을 사용하면서 기존 Devault 폰트·컬러·다크 모드 디자인 유지SettingsToggleRowSettingsPickerRowSettingsButtonRowSettingsLinkRowSettingsValueRowGeneral
SMAppService상태와 저장된 설정값 동기화Security
iCloud
Last update detected갱신Notifications
Data / About / Shortcuts
Architecture
SettingsRepository로 통합📸 스크린샷 (Optional)
🧪 테스트 / 검증 내역
tuist test전체 테스트 성공💬 기타 공유 사항
에서 진행합니다.
Refresh Status는 CloudKit 강제 동기화가 아니라 계정·네트워크·저장소 구성 상태를 다시 확인합니다.
Last update detected는 로컬 변경이 아닌 iCloud 원격 변경 감지 시각입니다.성 동작을 사용합니다.
니다. Copy 전용 payload 조회 연결을 TODO로 남겼습니다.
🙇🏻♀️ 리뷰 가이드 (선택)
SettingsRepository와 화면별 Settings UseCase/Client의 책임 분리가 적절한지LiveStorage와 Stable Repository Proxy를 이용한 iCloud 저장소 전환 구조AppFeature에서 관리하는 자동 잠금, 원격 변경 및 화면 캡처 보호 Stream 수명주기