Skip to content

Feature/#1 - Swift Data 모델링 및 AppSchema 구현#3

Open
dlguszoo wants to merge 14 commits into
developfrom
feature/#1/SwiftData_Modeling
Open

Feature/#1 - Swift Data 모델링 및 AppSchema 구현#3
dlguszoo wants to merge 14 commits into
developfrom
feature/#1/SwiftData_Modeling

Conversation

@dlguszoo
Copy link
Copy Markdown
Contributor

@dlguszoo dlguszoo commented May 7, 2026

✨ What’s this PR?

📌 관련 이슈 (Related Issue)


🧶 주요 변경 내용 (Summary)

  • DVData 모듈 Sources/Local/Models 폴더링
  • SwiftData 모델 변경에 대응하기 위해 AppSchema 기반의 스키마 버전 관리 구조 구현
  • AppAuditLog, BackupRecord, Project, Secret, SecretProjectLink, SecretPayload, SecretMetadata, SecretAuditLog 모델 구현

📸 스크린샷 (Optional)

설계 erd
  • 설계 erd

🧪 테스트 / 검증 내역

  • UI 정상 동작 확인
  • My Mac, macOS 13.0 환경에서 정상 동작

💬 기타 공유 사항


🙇🏻‍♀️ 리뷰 가이드 (선택)

  • Data 레이어 폴더링
  • AppSchema 구현 방식

Summary by CodeRabbit

  • New Features
    • 영구 저장 기능 강화: 프로젝트·시크릿 엔터티 추가 및 시크릿의 페이로드·메타데이터·프로젝트 연결, 시크릿/앱 감사 로그, 백업 기록을 포함한 통합 스키마 제공. 시크릿·프로젝트 관계, 링크 및 타임스탬프/상태 관리 개선.
  • Chores
    • 개발/리뷰 가이드라인 업데이트 (.coderabbit.yaml 설정 변경).

@dlguszoo dlguszoo requested a review from doyeonk429 May 7, 2026 06:21
@dlguszoo dlguszoo self-assigned this May 7, 2026
@dlguszoo dlguszoo added the ✨ Feature 새로운 기능 개발 label May 7, 2026
@dlguszoo dlguszoo linked an issue May 7, 2026 that may be closed by this pull request
8 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: String must contain at most 250 character(s) at "tone_instructions"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

이 PR은 Project/Secret 중심의 SwiftData 영속 계층을 도입하며, 링크·페이로드·메타데이터·감사·백업 모델을 추가하고 모든 모델을 Schema.appSchema(1.0.0)에 등록합니다.

Changes

SwiftData Domain Model Implementation

Layer / File(s) Summary
Core Domain Entities
Projects/DVData/Sources/Storage/Local/Models/Project.swift, Projects/DVData/Sources/Storage/Local/Models/Secret.swift
Project 및 Secret 모델 추가: 고유 식별자, 필드(타입/메타/상태/타임스탬프), 관계(SecretProjectLink 등) 및 계산 속성(secrets, projects).
Relationship & Payload Models
Projects/DVData/Sources/Storage/Local/Models/SecretProjectLink.swift, Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift, Projects/DVData/Sources/Storage/Local/Models/SecretMetadata.swift, Projects/DVData/Sources/Storage/Local/Models/SecretAuditLog.swift
SecretProjectLink(연결·타임스탬프), SecretPayload(암호화 데이터 + keyTag + schemaVersion), SecretMetadata(JSON 메타 + 버전), SecretAuditLog(이벤트·actorContext·의심 플래그) 추가.
Supporting Models
Projects/DVData/Sources/Storage/Local/Models/AppAuditLog.swift, Projects/DVData/Sources/Storage/Local/Models/BackupRecord.swift
AppAuditLog(앱 수준 이벤트), BackupRecord(백업 파일/경로/범위/키태그/시크릿 수 등) 추가.
Schema Definition
Projects/DVData/Sources/Storage/Local/Models/AppSchema.swift
모든 모델을 포함하는 Schema.appSchema(version 1.0.0) 정의 및 SwiftDataModel 네임스페이스 자리 표시자 추가.
Tooling / Reviewer Config
.coderabbit.yaml
리뷰 톤·지침을 한국어 리뷰자로 변경(자동화 가능한 스타일 권고 포함).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #2: Main issue implements the SwiftData model classes; 이 PR의 모델 목록이 해당 이슈의 요구 항목과 직접적으로 일치합니다.

코멘트 — 개선 제안 (간단)

  • 관계·삭제 규칙 가시성 부족: 여러 관계에 대해 deleteRule(.cascade/.nullify)와 optional 여부가 분산되어 있습니다. 각 모델 파일 상단에 관계 의도(예: 소유권, nullify 시나리오)와 마이그레이션 영향 간단 주석을 추가하세요. (참고: Apple SwiftData 관계 문서)
  • 중복 key 생성 로직: secretKey/linkKey를 생성자에서 반복하고 있습니다. 공용 팩토리(예: static func secretKey(from: Secret))로 중앙화해 중복을 줄이세요. (참고: Swift API Design Guidelines)
  • metadataJSON 스키마 진화 대비: raw Data 저장은 마이그레이션 부담을 키웁니다. Schema.Version 기반 마이그레이션 유닛 또는 버전별 디코더를 도입해 변환 경로를 명시하세요. (참고: Apple의 데이터 마이그레이션 권장사항)
  • 자동화 가능한 스타일: 접근 제어·주석·포맷 일관성이 일부 파일에서 다릅니다. SwiftLint/SwiftFormat을 CI에 도입해 규칙을 자동 적용하세요.

Poem

🐰 스키마의 터널이 반짝 열리네,
프로젝트와 비밀들이 손을 맞잡네.
모델, 연결, 감사, 백업까지 한 줄로,
데이터 집이 튼튼히 서 가는구나!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 PR의 주요 변경점인 SwiftData 모델링 및 AppSchema 구현을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed 모든 필수 모델(Secret, Project, SecretProjectLink, SecretPayload, SecretMetadata, SecretAuditLog, BackupRecord, AppAuditLog)이 구현되었고 AppSchema 스키마 버전 관리가 추가되었습니다.
Out of Scope Changes check ✅ Passed .coderabbit.yaml의 tone_instructions 변경을 제외한 모든 수정사항이 SwiftData 모델 정의 및 스키마 관리라는 목표에 부합합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#1/SwiftData_Modeling

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Projects/DVData/Sources/Storage/Local/Models/Secret.swift`:
- Around line 30-31: The Relationship on Secret.auditLogs currently uses
deleteRule: .cascade which hard-deletes SecretAuditLog entries when a Secret is
removed; update the relationship declaration on the Secret model (the auditLogs
property) to use deleteRule: .nullify so audit logs are orphaned rather than
deleted, keeping inverse: \SwiftDataModel.SecretAuditLog.secret and preserving
the existing soft-delete semantics (deletedAt) for compliance/audit purposes.
- Around line 48-49: The initializer for Secret currently uses Date() separately
for createdAt and updatedAt causing different timestamps; change the initializer
(init in Secret) to derive a single timestamp: accept createdAt: Date = Date()
and updatedAt: Date? = nil (or keep updatedAt optional/default) and inside the
init set self.updatedAt = updatedAt ?? createdAt so when updatedAt is not
provided it equals createdAt; apply the same pattern for the other similar
initializer/struct where createdAt/updatedAt defaults are declared.

In `@Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift`:
- Around line 13-22: Make the Secret relationship non-optional to prevent
orphaned child records: in SecretPayload change the `@Relationship` var secret:
Secret? to `@Relationship` var secret: Secret (remove the optional and the = nil
default in the init), update the initializer signature to require a Secret
parameter (remove secret: Secret? = nil and accept secret: Secret), and update
all call sites; apply the same change to SecretMetadata, SecretAuditLog, and
SecretProjectLink (remove ? and default nil, require Secret in their
initializers and fix usages).

In `@Projects/DVData/Sources/Storage/Local/Models/SecretProjectLink.swift`:
- Around line 10-24: SecretProjectLink's relationships (project and secret) are
optional but the init requires them and this allows orphaned links and
duplicates; change `@Relationship` var project: Project? and `@Relationship` var
secret: Secret? to required relationships (remove the optionals) and update the
initializer to assign the required non-optional members
(SecretProjectLink.init(project:secret:linkedAt:)) so the model cannot be
created without both sides. Also add a uniqueness constraint for the (project,
secret) pair—either add a composite unique index in the model/migration or
introduce a deterministic unique identifier (e.g., a composite id derived from
project+secret) to prevent duplicate join rows and avoid duplicate entries
appearing in Project.secrets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4818f012-16b1-4d39-9cbe-d6e9a8829266

📥 Commits

Reviewing files that changed from the base of the PR and between 613fadf and 0c0e53b.

📒 Files selected for processing (9)
  • Projects/DVData/Sources/Storage/Local/Models/AppAuditLog.swift
  • Projects/DVData/Sources/Storage/Local/Models/AppSchema.swift
  • Projects/DVData/Sources/Storage/Local/Models/BackupRecord.swift
  • Projects/DVData/Sources/Storage/Local/Models/Project.swift
  • Projects/DVData/Sources/Storage/Local/Models/Secret.swift
  • Projects/DVData/Sources/Storage/Local/Models/SecretAuditLog.swift
  • Projects/DVData/Sources/Storage/Local/Models/SecretMetadata.swift
  • Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift
  • Projects/DVData/Sources/Storage/Local/Models/SecretProjectLink.swift

Comment thread Projects/DVData/Sources/Storage/Local/Models/Secret.swift Outdated
Comment thread Projects/DVData/Sources/Storage/Local/Models/Secret.swift Outdated
Comment thread Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Projects/DVData/Sources/Storage/Local/Models/Secret.swift`:
- Around line 7-69: The Secret model and its members lack explicit access
control; annotate the class declaration and every stored property, the computed
property projects, and the init initializer with an explicit access level (e.g.,
internal or public depending on intended API exposure) so the intent is clear
(update the declaration of Secret, all vars like secretId, name, secretType,
subType, service, environment, expiresAt, memo, liked, deletedAt, createdAt,
updatedAt, projectLinks, payload, metadata, auditLogs, the computed property
projects, and the init signature to include the chosen access modifier); apply
the same explicit-access change pattern to AppAuditLog, BackupRecord, and
Project or add a SwiftLint/SwiftFormat rule to enforce it across models.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 70cae649-5b13-409b-ae43-fec018678929

📥 Commits

Reviewing files that changed from the base of the PR and between 9ab3eff and b315d60.

📒 Files selected for processing (1)
  • Projects/DVData/Sources/Storage/Local/Models/Secret.swift

Comment on lines +7 to +69
@Model final class Secret {
@Attribute(.unique) var secretId: 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]

var projects: [Project] {
projectLinks.compactMap(\.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 = []
self.auditLogs = []
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 접근 제어자 미명시 선언 탐지 (수정 후 결과가 없어야 정상)
rg -nP '^\s*(?!public\b|internal\b|private\b|fileprivate\b|open\b)(`@Model`\s+)?(final\s+class\b|var\b|init\s*\()' \
  Projects/DVData/Sources/Storage/Local/Models/Secret.swift

Repository: DevaultProject/Devault-macOS

Length of output: 676


🏁 Script executed:

# 확인 차원의 추가 검증: 코드베이스에서 유사한 SwiftData 모델들의 패턴 확인
find Projects/DVData/Sources/Storage/Local/Models -name "*.swift" -type f | head -5 | while read f; do
  echo "=== $f ==="
  head -20 "$f" | grep -E "(class|var|init)"
done

Repository: DevaultProject/Devault-macOS

Length of output: 1581


모델 선언부에 접근 제어자를 명시해 주세요.

Secret 클래스와 모든 프로퍼티, projects 계산 속성, init 메서드에 접근 제어자가 생략되어 있습니다. Swift 기본값은 internal이지만, 명시적으로 선언하면 모듈 API 경계가 명확해지고 SwiftData 모델 확장 시 의도가 분명해져 유지보수가 수월해집니다. Swift 공식 Access Control 가이드에서도 이를 권장합니다.

적용 예시
 extension SwiftDataModel {
-    `@Model` final class Secret {
-        `@Attribute`(.unique) var secretId: UUID
-        var name: String
+    `@Model` internal final class Secret {
+        `@Attribute`(.unique) internal var secretId: UUID
+        internal var name: String
         ...
-        var projects: [Project] {
+        internal var projects: [Project] {
             projectLinks.compactMap(\.project)
         }

-        init(
+        internal init(
             secretId: UUID = UUID(),
             ...
         ) {
             ...
         }
     }
 }

같은 패턴이 AppAuditLog, BackupRecord, Project 모델에도 적용되므로, SwiftLint 커스텀 룰이나 SwiftFormat 설정으로 전사(rollout)하는 것을 추천합니다. 이렇게 자동화하면 향후 신규 모델도 일관되게 관리할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVData/Sources/Storage/Local/Models/Secret.swift` around lines 7 -
69, The Secret model and its members lack explicit access control; annotate the
class declaration and every stored property, the computed property projects, and
the init initializer with an explicit access level (e.g., internal or public
depending on intended API exposure) so the intent is clear (update the
declaration of Secret, all vars like secretId, name, secretType, subType,
service, environment, expiresAt, memo, liked, deletedAt, createdAt, updatedAt,
projectLinks, payload, metadata, auditLogs, the computed property projects, and
the init signature to include the chosen access modifier); apply the same
explicit-access change pattern to AppAuditLog, BackupRecord, and Project or add
a SwiftLint/SwiftFormat rule to enforce it across models.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
Projects/DVData/Sources/Storage/Local/Models/SecretMetadata.swift (1)

7-27: ⚡ Quick win

접근 제어자를 각 선언부에 명시해주세요.

현재 SecretMetadata의 타입/프로퍼티/이니셜라이저가 모두 기본 접근 수준(internal)에 의존하고 있습니다. 팀 규칙상 선언부마다 접근 제어자를 명시하는 쪽이 좋습니다.

제안 패치
 extension SwiftDataModel {
-    `@Model` final class SecretMetadata {
-        `@Attribute`(.unique) var id: UUID
-        `@Attribute`(.unique) var secretKey: String
-        var metadataJSON: Data
-        var schemaVersion: Int
+    `@Model` internal final class SecretMetadata {
+        `@Attribute`(.unique) internal var id: UUID
+        `@Attribute`(.unique) internal var secretKey: String
+        internal var metadataJSON: Data
+        internal var schemaVersion: Int
 
         `@Relationship`
-        var secret: Secret
+        internal var secret: Secret
 
-        init(
+        internal init(
             id: UUID = UUID(),
             metadataJSON: Data,
             schemaVersion: Int,
             secret: Secret
         ) {

As per coding guidelines, "접근 제어자(access control)가 각 선언부에 명시적으로 기재되어 있는지 확인하세요."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVData/Sources/Storage/Local/Models/SecretMetadata.swift` around
lines 7 - 27, 명시적 접근 제어자가 빠져 있는 SecretMetadata 선언부와 멤버들에 접근 제어자(internal 또는 팀
규칙에 맞는 수준)를 추가하세요: 클래스 선언 SecretMetadata에 접근 제어자(internal final class
SecretMetadata)와 각 프로퍼티 id, secretKey, metadataJSON, schemaVersion, secret에 대해
개별 접근 제어자를 명시하고 생성자 init도 access modifier(internal init(...))를 붙여 선언부마다 명시적으로 접근
수준을 설정하도록 수정하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift`:
- Around line 7-30: The SecretPayload type and every declaration inside it
currently lack explicit access control; add the intended visibility (e.g.,
public or internal) to the class declaration, to each stored property (id,
secretKey, encryptedData, keyTag, schemaVersion, and the `@Relationship` var
secret) and to the initializer signature (init) so the module API surface is
explicit—update the declarations in SecretPayload accordingly (adjust to
public/internal consistent with DVData module API).

---

Nitpick comments:
In `@Projects/DVData/Sources/Storage/Local/Models/SecretMetadata.swift`:
- Around line 7-27: 명시적 접근 제어자가 빠져 있는 SecretMetadata 선언부와 멤버들에 접근 제어자(internal
또는 팀 규칙에 맞는 수준)를 추가하세요: 클래스 선언 SecretMetadata에 접근 제어자(internal final class
SecretMetadata)와 각 프로퍼티 id, secretKey, metadataJSON, schemaVersion, secret에 대해
개별 접근 제어자를 명시하고 생성자 init도 access modifier(internal init(...))를 붙여 선언부마다 명시적으로 접근
수준을 설정하도록 수정하세요.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fd9495eb-d8b9-48b7-9c68-60b31e5238fb

📥 Commits

Reviewing files that changed from the base of the PR and between b315d60 and adbede5.

📒 Files selected for processing (6)
  • Projects/DVData/Sources/Storage/Local/Models/Project.swift
  • Projects/DVData/Sources/Storage/Local/Models/Secret.swift
  • Projects/DVData/Sources/Storage/Local/Models/SecretAuditLog.swift
  • Projects/DVData/Sources/Storage/Local/Models/SecretMetadata.swift
  • Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift
  • Projects/DVData/Sources/Storage/Local/Models/SecretProjectLink.swift
✅ Files skipped from review due to trivial changes (1)
  • Projects/DVData/Sources/Storage/Local/Models/SecretProjectLink.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • Projects/DVData/Sources/Storage/Local/Models/Project.swift
  • Projects/DVData/Sources/Storage/Local/Models/SecretAuditLog.swift
  • Projects/DVData/Sources/Storage/Local/Models/Secret.swift

Comment on lines +7 to +30
@Model final class SecretPayload {
@Attribute(.unique) var id: UUID
@Attribute(.unique) var secretKey: String
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.secretKey = secret.secretId.uuidString
self.encryptedData = encryptedData
self.keyTag = keyTag
self.schemaVersion = schemaVersion
self.secret = secret
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

모든 선언부에 접근 제어자(access control) 명시 필요

클래스 자체(SecretPayload)와 모든 저장 프로퍼티, init 어느 곳에도 접근 제어자가 명시되어 있지 않습니다. Swift에서 기본값은 internal이지만, 코딩 가이드라인에 따르면 각 선언부에 반드시 명시해야 합니다. DVData 모듈의 공개 API 경계를 명확히 하기 위해, 의도한 가시성(예: public/internal)을 선언에 직접 기재해주세요.

✏️ 개선 예시 (의도한 접근 수준에 맞게 조정)
 extension SwiftDataModel {
-    `@Model` final class SecretPayload {
-        `@Attribute`(.unique) var id: UUID
-        `@Attribute`(.unique) var secretKey: String
-        var encryptedData: Data
-        var keyTag: String
-        var schemaVersion: Int
+    `@Model` public final class SecretPayload {
+        `@Attribute`(.unique) public var id: UUID
+        `@Attribute`(.unique) public var secretKey: String
+        public var encryptedData: Data
+        public var keyTag: String
+        public var schemaVersion: Int

         `@Relationship`
-        var secret: Secret
+        public var secret: Secret

-        init(
+        public init(
             id: UUID = UUID(),
             encryptedData: Data,
             keyTag: String,
             schemaVersion: Int,
             secret: Secret
         ) {

As per coding guidelines, "접근 제어자(access control)가 각 선언부에 명시적으로 기재되어 있는지 확인하세요."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Projects/DVData/Sources/Storage/Local/Models/SecretPayload.swift` around
lines 7 - 30, The SecretPayload type and every declaration inside it currently
lack explicit access control; add the intended visibility (e.g., public or
internal) to the class declaration, to each stored property (id, secretKey,
encryptedData, keyTag, schemaVersion, and the `@Relationship` var secret) and to
the initializer signature (init) so the module API surface is explicit—update
the declarations in SecretPayload accordingly (adjust to public/internal
consistent with DVData module API).

@doyeonk429 doyeonk429 force-pushed the feature/#1/SwiftData_Modeling branch from 8bfe9b2 to 8874a53 Compare May 7, 2026 13:24
var occurredAt: Date

@Relationship
var secret: Secret
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.

SecretAuditLog.secret이 non-optional이라 Secret에서 deleteRule: .nullify 옵션 사용할 경우에 런타임 충돌이나 저장 실패 가능해집니다. var secret: Secret? 로 바꾸는게 맞을 것 같아요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

.nullify의 의도는 "보안/감시 로그는 Secret 삭제시에도 보존해야 한다"로 결정했으므로 옵셔널로 수정하겠습니다! 다만 Secret 삭제시, 관계가 끊기기 때문에 어떤 Secret에 대한 로그였는지 알기 위한 스냅샷 필드 추가 고려중인데 의견이 궁금합니다!

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.

스냅샷 필드 추가 좋아용. 최소한 식별용으로 secretId(UUID), secretName, secretType 정도가 포함되면 좋을 것 같네요.

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.

스냅샷 필드를 채우는 시점도 고민해보면 좋을 것 같은데, 일단 안전하게 로그 생성 시점에 하는 거 어떨까요?

Copy link
Copy Markdown
Contributor

@doyeonk429 doyeonk429 left a comment

Choose a reason for hiding this comment

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

전부 다 반영할 필요는 없으니 accept할 것만 받아들이기! 👍

Comment on lines +11 to +15
@Relationship
var project: Project

@Relationship
var secret: Secret
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.

링크 자체가 직접 삭제될 때의 동작이 정의되어 있지 않고, 양쪽이 non-optional이라 단독 삭제 시 무결성이 깨질 수 있습니다. Project/Secret 쪽 cascade에 의해서만 삭제되도록 옵셔널 처리 강화가 필요해보입니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Project/Secret 쪽 cascade에 의해서만 삭제되는 것이 아닌 Project에서 Secret을 제거하는 경우도 고려해서 설계하였습니다! 이 동작은 Secret 삭제가 아니라 Project-Secret 연결 해제로 정의했는데, 이때 실제로 삭제되는 것은 SecretProjectLink(Project, Secret) row뿐이고, ProjectSecret 자체는 유지됩니다. Secret이 다른 Project에도 연결되어 있다면 그 연결도 그대로 유지됩니다. 이 경우 link row 자체가 삭제되기 때문에 projectsecret을 nil로 만들 필요가 없습니다. 오히려 optional로 두면 한쪽 endpoint가 없는 invalid link row가 저장될 수 있어, join row의 양 끝점은 non-optional로 유지하는 것이 맞다고 봤습니다.
또한 Project 또는 Secret이 삭제되는 경우에도 cascade 대상은 반대편 모델이 아니라 연결된 SecretProjectLink row로 한정하려고 합니다.

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.

상세한 설계 의도 설명 감사합니다 🙏
Project 또는 Secret이 삭제될 때 연결된 SecretProjectLink row가 같이 정리되도록 양쪽 모두에 deleteRule: .cascade로 설정되어야할 것 같네요 😸

secret: Secret
) {
self.id = id
self.secretKey = secret.secretId.uuidString
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.

Secret과 1:1 관계이므로 secret 관계만으로도 무결성 확보가 가능하고 secretKey는 파생값으로 보이는데, 현재 코드 기준으로는 secretKey가 초기화 시점에만 동기화되고 이후 secret.secretId가 변경돼도 자동 갱신되지 않는데 이렇게 되면 단방향 일관성만 보장됩니다.


extension SwiftDataModel {
@Model final class Secret {
@Attribute(.unique) var secretId: UUID
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.

secretId만 id가 아닌 네이밍으로 지은 이유가 있나요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

이 부분 수정하겠습니다~!

@Attribute(.unique) var id: UUID
var fileName: String
var filePath: String
var backupScope: String
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.

이렇게 도메인이 한정적인 필드의 경우는 enum + String 저장 형태로 바꾸는 것이 더 안전해보입니다. RawRepresentable 참고

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.

extension SwiftDataModel {
    enum BackupScope: String, Codable, CaseIterable, Sendable {
        case optionA
        case optionB
        case optionC

        var displayName: String {
            switch self {
            case .optionA: "옵션 A"
            case .optionB: "옵션 B"
            case .optionC: "옵션 C"
            }
        }
    }
}

enum SwiftDataModel { }

extension Schema {
private static let actualVersion: Schema.Version = Version(1, 0, 0)
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.

actual의 의미가 좀 모호한 것 같아요. currentVersion 또는 schemaVersion 같은 네이밍이 좋을 것 같아요.(이건 취향차이긴 함)

extension Schema {
private static let actualVersion: Schema.Version = Version(1, 0, 0)

static var appSchema: Schema {
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.

appSchema는 호출 시마다 Schema를 생성하는 것 같은데 캐싱하면 어떨까요?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Data Module Swift Model 정의

2 participants