Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
43a604b
[#81] refactor: FetchSecretUseCase에서 RevealSecretPayloadUseCase 분리
dlguszoo Aug 10, 2026
c84cafe
[#89] feat: 메인 화면에 잠금 버튼 추가
dlguszoo Aug 14, 2026
d0c97ab
[#89] refactor: 보안 알림 문구 생성을 Presentation 계층으로 이동
dlguszoo Aug 14, 2026
add38f1
[#89] feat: 온보딩 iCloud 동기화 실패 처리 및 성공 화면 추가
dlguszoo Aug 14, 2026
484de1e
[#89] feat: 사이드바 로컬라이제이션 및 접근성 보강
dlguszoo Aug 14, 2026
11fc497
[#89] fix: 인증 실패 alert 문구 로컬라이제이션
dlguszoo Aug 14, 2026
2eb961f
[#89] fix: 비활성 버튼이 과도하게 밝게 보이던 문제 수정
dlguszoo Aug 14, 2026
5f81c34
[#89] chore: String Catalog 추출 설정 활성화
dlguszoo Aug 14, 2026
afca293
[#89] chore: Localizable.xcstrings 카탈로그 동기화
dlguszoo Aug 14, 2026
cd2f522
[#89] fix: Localizable.xcstrings 추가
dlguszoo Aug 14, 2026
5d5b833
[#89] fix: iCloud 동기화 성공 화면 문구 보강
dlguszoo Aug 14, 2026
a80be9d
[#89] fix: 잠금 해제 실패 alert 제목 로컬라이제이션
dlguszoo Aug 14, 2026
8c88ac4
[#89] refactor: iCloud 동기화 alert의 재시도/설정 열기 여부를 exhaustive switch로 통합
dlguszoo Aug 14, 2026
5a695d9
[#89] refactor: public extension 대신 선언별 접근 제어 사용
dlguszoo Aug 14, 2026
23593e0
[#89] fix: 온보딩 전 만료 알림 동기화가 ModelContainer를 조기 고정하던 문제 수정
dlguszoo Aug 14, 2026
065ede0
[#89] fix: Localizable.xcstrings 추가
dlguszoo Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import DVDomain

public struct SecurityNotificationServiceImpl: SecurityNotificationService {
private let center: UNUserNotificationCenter
private let makeContent: @Sendable (SecurityNotification) -> (title: String, body: String)

public init(center: UNUserNotificationCenter = .current()) {
/// `makeContent`는 Data 모듈에서 Presentation의 로컬라이제이션 카탈로그에 접근할 수 없어 외부에서 주입받는다.
/// 이 타입이 직접 가질 수 없는 관심사를 순수 함수로 밖에서 받는다.
public init(
center: UNUserNotificationCenter = .current(),
makeContent: @escaping @Sendable (SecurityNotification) -> (title: String, body: String)
) {
self.center = center
self.makeContent = makeContent
}

public func requestAuthorization() async throws -> Bool {
Expand All @@ -23,7 +30,7 @@ public struct SecurityNotificationServiceImpl: SecurityNotificationService {
public func notify(_ notification: SecurityNotification) async throws {
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: Self.makeContent(for: notification),
content: makeUNContent(for: notification),
trigger: nil // trigger가 nil이면 즉시 발송
)
do {
Expand All @@ -45,7 +52,7 @@ public struct SecurityNotificationServiceImpl: SecurityNotificationService {
)
let notificationRequest = UNNotificationRequest(
identifier: request.identifier,
content: Self.makeContent(for: request.notification),
content: makeUNContent(for: request.notification),
trigger: trigger
)
do {
Expand All @@ -63,21 +70,11 @@ public struct SecurityNotificationServiceImpl: SecurityNotificationService {
// MARK: - Private

private extension SecurityNotificationServiceImpl {
static func makeContent(for notification: SecurityNotification) -> UNMutableNotificationContent {
func makeUNContent(for notification: SecurityNotification) -> UNMutableNotificationContent {
let (title, body) = makeContent(notification)
let content = UNMutableNotificationContent()
switch notification {
case .abnormalAccess(let reason):
content.title = "비정상 접근이 감지됐어요"
content.body = reason

case .clipboardExceeded(let seconds):
content.title = "클립보드를 정리했어요"
content.body = "복사된 값이 \(seconds)초 넘게 남아 있어 자동으로 지웠어요."

case .secretExpiresSoon(_, let daysBefore):
content.title = "Secret 만료가 다가와요"
content.body = "저장된 Secret이 \(daysBefore)일 후 만료돼요."
}
content.title = title
content.body = body
content.sound = .default
return content
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ extension DVButtonPreviewView {
DVButton(titleText: "Cancel", style: .secondary) {}
.disabled(true)
}
.frame(width: 74)
}

private var buttonPair: some View {
Expand Down
12 changes: 11 additions & 1 deletion Projects/DVDesign/Sources/Components/DVButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ private struct DVButtonStyle: ButtonStyle {
.foregroundStyle(foregroundColor)
.background(backgroundColor(isPressed: configuration.isPressed))
.clipShape(RoundedRectangle(cornerRadius: style.cornerRadius))
.opacity(!isEnabled && dimsWhenDisabled ? 0.5 : 1)
}

/// primary/primarySmall은 배경이 항상 vaultGreen이라 비활성 시 opacity로 톤을 낮춘다.
/// secondary/secondaryProminent는 이미 자체 비활성 색상(gray400 등)이 있어 중복 적용하지 않는다.
private var dimsWhenDisabled: Bool {
switch style {
case .primary, .primarySmall: return true
case .secondary, .secondaryProminent: return false
}
}

private var foregroundColor: Color {
Expand All @@ -126,7 +136,7 @@ private struct DVButtonStyle: ButtonStyle {
private func backgroundColor(isPressed: Bool) -> Color {
switch style {
case .primary, .primarySmall:
if !isEnabled { return Color.dv(.vaultGreenTint) }
if !isEnabled { return Color.dv(.vaultGreen) }
if isPressed || isHovered { return Color.dv(.vaultGreenDark) }
return Color.dv(.vaultGreen)
case .secondary:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import Foundation

/// 로컬 알림으로 사용자에게 전달할 보안 이벤트입니다.
public enum SecurityNotification: Equatable, Sendable {
/// 잠금 해제 반복 실패 등 비정상적인 접근이 감지됨
case abnormalAccess(reason: String)
/// 잠금 해제 반복 실패 등 비정상적인 접근이 감지됨. 문구는 소비처(Presentation)가 만드므로 여기선 원인 판단에 필요한 값만 들고 있는다.
case abnormalAccess(kind: AbnormalAccessKind, threshold: Int)
/// 클립보드에 민감 값이 30초 이상 남아 있어 정리함
case clipboardExceeded(seconds: Int)
/// Secret이 곧 만료됨. 인증 없이 노출될 수 있어 name은 포함하지 않음.
case secretExpiresSoon(secretID: UUID, daysBefore: Int)
}

/// `abnormalAccess`를 유발한 반복 행위의 종류입니다. 문구는 소비처가 만들고, 여기선 분기 값만 제공합니다.
public enum AbnormalAccessKind: Equatable, Sendable {
case authenticationFailure
case repeatedCopy
}

/// 특정 시각에 발송되도록 예약하는 알림 요청입니다.
public struct ScheduledSecurityNotification: Equatable, Sendable {
public let identifier: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public actor AuthenticateUseCaseImpl: AuthenticateUseCase {
if abnormalAccessMonitor.recordAccess(at: now()) {
do {
try await notificationService.notify(
.abnormalAccess(reason: "짧은 시간 안에 인증 실패가 \(Self.abnormalAccessThreshold)회 이상 반복됨")
.abnormalAccess(kind: .authenticationFailure, threshold: Self.abnormalAccessThreshold)
)

// alert에 상관없이 알림 스킵을 막기 위해 약간의 지연 추가
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public actor CopySensitiveValueUseCaseImpl: CopySensitiveValueUseCase {
if abnormalAccessMonitor.recordAccess(at: now()) {
do {
try await notificationService.notify(
.abnormalAccess(reason: "짧은 시간 안에 값 복사가 \(Self.abnormalAccessThreshold)회 이상 반복됨")
.abnormalAccess(kind: .repeatedCopy, threshold: Self.abnormalAccessThreshold)
)
} catch {
// 알림 실패는 복사 자체를 실패시키면 안 되므로 로깅만
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ struct AuthenticateUseCaseImplTests {

#expect(notificationService.notified.count == 1)
#expect(notificationService.notified.first == .abnormalAccess(
reason: "짧은 시간 안에 인증 실패가 3회 이상 반복됨"
kind: .authenticationFailure, threshold: 3
))
}

Expand Down
7 changes: 6 additions & 1 deletion Projects/DVPresentation/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ let project = Project.project(
// 3rd-party dependency
.tca(),
.lottie(),
]
],
// 기본값(NO)이면 String.module(_:) 콜사이트가 Localizable.xcstrings로 추출되지 않는다.
settings: .settings(base: [
"SWIFT_EMIT_LOC_STRINGS": "YES",
"LOCALIZATION_PREFERS_STRING_CATALOGS": "YES",
])
),
.tests(
name: "DVPresentationTests",
Expand Down
Loading