diff --git a/.coderabbit.yaml b/.coderabbit.yaml index e2ff6cf..0f3084e 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -6,7 +6,6 @@ language: "ko-KR" early_access: false enable_free_tier: true - tone_instructions: "Apple 플랫폼 시니어 개발자 관점으로, 원인과 개선안을 짧고 명확히 제시하며 개선 중심으로 리뷰하세요." # ============================================================================= diff --git a/Projects/DVData/Sources/Storage/Local/Models/AppAuditLog.swift b/Projects/DVData/Sources/Storage/Local/Models/AppAuditLog.swift new file mode 100644 index 0000000..ad1a92a --- /dev/null +++ b/Projects/DVData/Sources/Storage/Local/Models/AppAuditLog.swift @@ -0,0 +1,25 @@ +// Copyright © 2026 Devault. All rights reserved + +import Foundation +import SwiftData + +extension SwiftDataModel { + @Model final class AppAuditLog { + @Attribute(.unique) var id: UUID + var eventType: String + var actorContext: String + var occurredAt: Date + + init( + id: UUID = UUID(), + eventType: String, + actorContext: String, + occurredAt: Date = Date() + ) { + self.id = id + self.eventType = eventType + self.actorContext = actorContext + self.occurredAt = occurredAt + } + } +} diff --git a/Projects/DVData/Sources/Storage/Local/Models/AppSchema.swift b/Projects/DVData/Sources/Storage/Local/Models/AppSchema.swift new file mode 100644 index 0000000..bbde077 --- /dev/null +++ b/Projects/DVData/Sources/Storage/Local/Models/AppSchema.swift @@ -0,0 +1,20 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftData + +enum SwiftDataModel { } + +extension Schema { + private static let schemaVersion: Schema.Version = Version(1, 0, 0) + + static let appSchema = Schema([ + SwiftDataModel.Project.self, + SwiftDataModel.Secret.self, + SwiftDataModel.SecretProjectLink.self, + SwiftDataModel.SecretPayload.self, + SwiftDataModel.SecretMetadata.self, + SwiftDataModel.SecretAuditLog.self, + SwiftDataModel.AppAuditLog.self, + SwiftDataModel.BackupRecord.self, + ], version: schemaVersion) +} diff --git a/Projects/DVData/Sources/Storage/Local/Models/BackupRecord.swift b/Projects/DVData/Sources/Storage/Local/Models/BackupRecord.swift new file mode 100644 index 0000000..0a543dd --- /dev/null +++ b/Projects/DVData/Sources/Storage/Local/Models/BackupRecord.swift @@ -0,0 +1,37 @@ +// Copyright © 2026 Devault. All rights reserved + +import Foundation +import SwiftData + +extension SwiftDataModel { + @Model final class BackupRecord { + @Attribute(.unique) var id: UUID + var fileName: String + var filePath: String + var backupScope: String + var hasIndependentPassword: Bool + var keyTag: String + var totalSecrets: Int + var createdAt: Date + + init( + id: UUID = UUID(), + fileName: String, + filePath: String, + backupScope: String, + hasIndependentPassword: Bool, + keyTag: String, + totalSecrets: Int, + createdAt: Date = Date() + ) { + self.id = id + self.fileName = fileName + self.filePath = filePath + self.backupScope = backupScope + self.hasIndependentPassword = hasIndependentPassword + self.keyTag = keyTag + self.totalSecrets = totalSecrets + self.createdAt = createdAt + } + } +} diff --git a/Projects/DVData/Sources/Storage/Local/Models/Project.swift b/Projects/DVData/Sources/Storage/Local/Models/Project.swift new file mode 100644 index 0000000..33887e4 --- /dev/null +++ b/Projects/DVData/Sources/Storage/Local/Models/Project.swift @@ -0,0 +1,34 @@ +// Copyright © 2026 Devault. All rights reserved + +import Foundation +import SwiftData + +extension SwiftDataModel { + @Model final class Project { + @Attribute(.unique) var id: UUID + var name: String + var createdAt: Date + var updatedAt: Date + + @Relationship(deleteRule: .cascade, inverse: \SwiftDataModel.SecretProjectLink.project) + var secretLinks: [SecretProjectLink] + + /// `SecretProjectLink`를 통해 연결된 시크릿 목록을 반환하는 편의 접근자입니다. + var secrets: [Secret] { + secretLinks.map(\.secret) + } + + init( + id: UUID = UUID(), + name: String, + createdAt: Date = Date(), + updatedAt: Date = Date() + ) { + self.id = id + self.name = name + self.createdAt = createdAt + self.updatedAt = updatedAt + self.secretLinks = [] + } + } +} diff --git a/Projects/DVData/Sources/Storage/Local/Models/Secret.swift b/Projects/DVData/Sources/Storage/Local/Models/Secret.swift new file mode 100644 index 0000000..6861923 --- /dev/null +++ b/Projects/DVData/Sources/Storage/Local/Models/Secret.swift @@ -0,0 +1,71 @@ +// Copyright © 2026 Devault. All rights reserved + +import SwiftData +import Foundation + +extension SwiftDataModel { + @Model final class Secret { + @Attribute(.unique) var id: UUID + var name: String + var secretType: String + var subType: String? + var service: String? + var environment: String? + var expiresAt: Date? + var memo: String? + var liked: Bool + var deletedAt: Date? + var createdAt: Date + var updatedAt: Date + + @Relationship(deleteRule: .cascade, inverse: \SwiftDataModel.SecretProjectLink.secret) + var projectLinks: [SecretProjectLink] + + @Relationship(deleteRule: .cascade, inverse: \SwiftDataModel.SecretPayload.secret) + var payload: SecretPayload? + + @Relationship(deleteRule: .cascade, inverse: \SwiftDataModel.SecretMetadata.secret) + var metadata: SecretMetadata? + + @Relationship(deleteRule: .nullify, inverse: \SwiftDataModel.SecretAuditLog.secret) + var auditLogs: [SecretAuditLog] + + /// `SecretProjectLink`를 통해 연결된 프로젝트 목록을 반환하는 편의 접근자입니다. + var projects: [Project] { + projectLinks.map(\.project) + } + + init( + secretId: UUID = UUID(), + name: String, + secretType: String, + subType: String? = nil, + service: String? = nil, + environment: String? = nil, + expiresAt: Date? = nil, + memo: String? = nil, + liked: Bool = false, + deletedAt: Date? = nil, + createdAt: Date = Date(), + updatedAt: Date? = nil + ) { + let initialCreatedAt = createdAt + self.secretId = secretId + self.name = name + self.secretType = secretType + self.subType = subType + self.service = service + self.environment = environment + self.expiresAt = expiresAt + self.memo = memo + self.liked = liked + self.deletedAt = deletedAt + self.createdAt = initialCreatedAt + self.updatedAt = updatedAt ?? initialCreatedAt + self.projectLinks = [] + self.payload = nil + self.metadata = nil + self.auditLogs = [] + } + } +} diff --git a/Projects/DVData/Sources/Storage/Local/Models/SecretAuditLog.swift b/Projects/DVData/Sources/Storage/Local/Models/SecretAuditLog.swift new file mode 100644 index 0000000..b5968df --- /dev/null +++ b/Projects/DVData/Sources/Storage/Local/Models/SecretAuditLog.swift @@ -0,0 +1,41 @@ +// Copyright © 2026 Devault. All rights reserved + +import Foundation +import SwiftData + +extension SwiftDataModel { + @Model final class SecretAuditLog { + @Attribute(.unique) var id: UUID + var eventType: String + var actorContext: String + var isSuspicious: Bool + var occurredAt: Date + + // MARK: - Snapshot field + var secretSnapshotId: UUID + var secretNameSnapshot: String + var secretTypeSnapshot: String + + @Relationship + var secret: Secret? + + init( + id: UUID = UUID(), + eventType: String, + actorContext: String, + isSuspicious: Bool = false, + occurredAt: Date = Date(), + secret: Secret + ) { + self.id = id + self.eventType = eventType + self.actorContext = actorContext + self.isSuspicious = isSuspicious + self.occurredAt = occurredAt + self.secretSnapshotId = secret.id + self.secretNameSnapshot = secret.name + self.secretTypeSnapshot = secret.secretType + self.secret = secret + } + } +} diff --git a/Projects/DVData/Sources/Storage/Local/Models/SecretMetadata.swift b/Projects/DVData/Sources/Storage/Local/Models/SecretMetadata.swift new file mode 100644 index 0000000..284b29f --- /dev/null +++ b/Projects/DVData/Sources/Storage/Local/Models/SecretMetadata.swift @@ -0,0 +1,27 @@ +// Copyright © 2026 Devault. All rights reserved + +import Foundation +import SwiftData + +extension SwiftDataModel { + @Model final class SecretMetadata { + @Attribute(.unique) var id: UUID + var metadataJSON: Data + var schemaVersion: Int + + @Relationship + var secret: Secret + + init( + id: UUID = UUID(), + metadataJSON: Data, + schemaVersion: Int, + secret: Secret + ) { + self.id = id + self.metadataJSON = metadataJSON + self.schemaVersion = schemaVersion + self.secret = secret + } + } +} diff --git a/Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift b/Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift new file mode 100644 index 0000000..f503ce0 --- /dev/null +++ b/Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift @@ -0,0 +1,30 @@ +// Copyright © 2026 Devault. All rights reserved + +import Foundation +import SwiftData + +extension SwiftDataModel { + @Model final class SecretPayload { + @Attribute(.unique) var id: UUID + var encryptedData: Data + var keyTag: String + var schemaVersion: Int + + @Relationship + var secret: Secret + + init( + id: UUID = UUID(), + encryptedData: Data, + keyTag: String, + schemaVersion: Int, + secret: Secret + ) { + self.id = id + self.encryptedData = encryptedData + self.keyTag = keyTag + self.schemaVersion = schemaVersion + self.secret = secret + } + } +} diff --git a/Projects/DVData/Sources/Storage/Local/Models/SecretProjectLink.swift b/Projects/DVData/Sources/Storage/Local/Models/SecretProjectLink.swift new file mode 100644 index 0000000..08d18aa --- /dev/null +++ b/Projects/DVData/Sources/Storage/Local/Models/SecretProjectLink.swift @@ -0,0 +1,28 @@ +// Copyright © 2026 Devault. All rights reserved + +import Foundation +import SwiftData + +extension SwiftDataModel { + @Model final class SecretProjectLink { + @Attribute(.unique) var linkKey: String + var linkedAt: Date + + @Relationship + var project: Project + + @Relationship + var secret: Secret + + init( + project: Project, + secret: Secret, + linkedAt: Date = Date() + ) { + self.linkKey = "\(project.id.uuidString):\(secret.id.uuidString)" + self.linkedAt = linkedAt + self.project = project + self.secret = secret + } + } +}