Skip to content

Feature/#25 - Project CRUD ORM과 Secret-Project 링크 메서드 구현#27

Open
dlguszoo wants to merge 24 commits into
developfrom
feature/#25
Open

Feature/#25 - Project CRUD ORM과 Secret-Project 링크 메서드 구현#27
dlguszoo wants to merge 24 commits into
developfrom
feature/#25

Conversation

@dlguszoo

@dlguszoo dlguszoo commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

✨ What’s this PR?

📌 관련 이슈 (Related Issue)


🧶 주요 변경 내용 (Summary)

  1. Project ORM
  • Project Domain entity 추가
  • ProjectRepository interface 정의 및 SwiftData 기반 ProjectRepositoryImpl 구현
    • create
    • fetch(id:)
    • fetchAll
    • patch
    • delete
  • ProjectPatchProjectRepositoryError 추가
  • ProjectUseCase Interface 정의 및 구현
    • CreateProjectUseCase
    • FetchProjectUseCase
    • RenameProjectUseCase
    • DeleteProjectUseCase
  • SwiftData Project 모델을 Domain Project로 변환하는 mapper 구현
  1. Secret-Project 링크 메서드
  • SecretRepository에 Project 관계 메서드를 추가
    • fetchProjects(secretID:)
    • linkProject(secretID:projectID:)
    • unlinkProject(secretID:projectID:)
  • SecretRepositoryError에 Project 관계 관련 error 추가
    • projectNotFound
    • duplicateProjectLink
    • projectLinkNotFound
  • SecretRepositoryImpl에서 SecretProjectLink 기반 link/unlink를 구현
  • SecretQuery.Collection.project(id:)를 통해 Project 기준 Secret 조회 흐름 유지
  • FetchSecretUseCase: fetchProjects(secretID:) 추가, CreateSecretUseCase: projectIDs: [UUID] 파라미터 통합, PatchSecretUseCase: 전체 업데이트 update 메서드 추가, SecretProjectRelationUseCase에서 link/unlink 메서드 구현
  1. SecretType / SecretSubType enum 도입
  • SwiftData 모델은 rawValue String 유지, toDomain() 변환 시 enum 복원

📸 스크린샷 (Optional)

스크린샷 2026-07-01 오후 5 47 47

🧪 테스트 / 검증 내역

  • tuist build Devault 성공
  • 임시 demo view에서 흐름 확인 (project, secret생성 및 연결, project 기준 가진 secret 목록 조회, secret 속한 project 목록 조회, 선택한 secret-project 관계 해제
  • My Mac, macOS 13.0 환경에서 정상 동작

💬 기타 공유 사항

  • Domain entity에는 Project.secrets, Secret.projects 같은 관계 배열을 두지 않았습니다.
  • SwiftData의 다대다 관계는 저장 구조로만 사용하고, Domain 관계 조회는 명시적인 repository/usecase API로 분리했습
    니다.
  • Project 목록은 검색/필터 없이 이름 오름차순으로 고정했습니다.
  • Project 이름 중복은 lowercased() 기준으로 비교합니다.
  • 저장되는 Project 이름은 lowercased 값이 아니라, UseCase에서 trim된 원본 이름을 유지합니다.
    • 즉, Project 이름 생성/변경 시 앞뒤 공백과 줄바꿈은 제거합니다.
  • Project 수정은 현재 name만 가능하지만, 이후 확장성(pinned 등)을 고려해 ProjectPatchpatch(id:with:) 구조를 유지했습
    니다.
  • Secret-Project 링크 변경 API는 ProjectRepository에도 중복 정의하지 않고 SecretRepository로 단일화했습니다.

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

update 시 patch-reconcile 간 일치성 보장 문제

  • 문제 상황
    PatchSecretUseCaseImpl.update는 내부적으로 두 단계로 실행됩니다.
  1. repository.patch(id:with:) → 일반 필드 저장
  2. reconcileProjects(...) → Project link/unlink 조정

두 호출이 별개의 ModelContext.save()로 분리되어 있어, patch 성공 후 reconcileProjects가 실패하면 필드는 수정됐지만 Project 연결은 이전 상태로 남는 partial state가 발생할 수 있습니다.

  • UseCase 레벨 rollback을 채택하지 않은 이유
    rollback용 SecretPatch(from: originalSecret) 역변환 로직을 별도로 설계해야 하는 비용이 있고, rollback 자체도 실패할 경우 오히려 상태를 알 수 없게 됩니다. SwiftData 로컬 환경에서 patch 성공 후 reconcileProjects만 실패하는 케이스가 현실적으로 거의 없습니다.

  • 근본적 해결 방향 (추후 논의)
    patch와 link/unlink를 하나의 ModelContext.save() 트랜잭션으로 묶는 것이 근본적인 해결책인것 같습니다. 현재 Repository 인터페이스가 개별 작업 단위로 설계되어 있어 트랜잭션 묶음을 지원하려면 인터페이스 재설계가 필요하므로, Repository 계층 고도화 시 함께 논의해야할 것 같습니다.

Summary by CodeRabbit

  • New Features

    • 프로젝트 관리 기능이 추가되어 생성, 조회, 수정, 삭제를 지원합니다.
    • 시크릿에 프로젝트를 연결·해제하고, 시크릿과 연결된 프로젝트를 확인할 수 있습니다.
    • 프로젝트-시크릿 관계를 다루는 새 데모 화면이 추가되었습니다.
    • 시크릿 유형과 하위 유형 선택이 더 명확해졌습니다.
  • Bug Fixes

    • 검색 시 시크릿 유형/하위 유형이 일관되게 반영되도록 개선되었습니다.
    • 이름 입력값의 공백 처리와 중복 검사가 더 안정적으로 동작합니다.

dlguszoo added 23 commits June 30, 2026 16:01
@dlguszoo dlguszoo self-assigned this Jul 1, 2026
@dlguszoo dlguszoo added the ✨ Feature 새로운 기능 개발 label Jul 1, 2026
@dlguszoo dlguszoo linked an issue Jul 1, 2026 that may be closed by this pull request
6 tasks
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dlguszoo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 278d9ff0-c96f-42e7-881a-8907c1f67a47

📥 Commits

Reviewing files that changed from the base of the PR and between 9f889d8 and f12befb.

📒 Files selected for processing (1)
  • .github/PULL_REQUEST_TEMPLATE.md

Walkthrough

Project 도메인 엔티티, 저장소 인터페이스/구현체(SwiftData), 유스케이스(CRUD)를 신규 도입하고, Secret의 secretType/subType을 문자열에서 열거형(SecretType/SecretSubType)으로 전환했습니다. Secret-Project 연결/해제 유스케이스와 데모 UI, 앱 와이어링이 함께 추가·수정되었습니다.

Changes

Project CRUD 및 Secret-Project 연결

Layer / File(s) Summary
도메인 엔티티 및 데이터 계약
Projects/DVDomain/Sources/Entity/Project.swift, Projects/DVDomain/Sources/Entity/Secret.swift, Projects/DVDomain/Sources/Entity/SecretType.swift, Projects/DVDomain/Sources/Repository/PatchField.swift, Projects/DVDomain/Sources/Repository/ProjectPatch.swift, Projects/DVDomain/Sources/Repository/SecretPatch.swift, Projects/DVDomain/Sources/Repository/Error/*RepositoryError.swift, Projects/DVDomain/Sources/UseCase/Draft/SecretDraft.swift
Project 엔티티, SecretType/SecretSubType 열거형, PatchField 제네릭 및 ProjectPatch/SecretPatch를 신설·전환하고 저장소 에러에 프로젝트/링크 관련 케이스를 추가함.
저장소/서비스 인터페이스 및 문서화
Projects/DVDomain/Sources/Repository/Interface/*.swift, Projects/DVDomain/Sources/Service/Interface/*.swift
ProjectRepository 프로토콜을 신설하고 SecretRepository, SecretCryptoService, UserAuthenticationService에 문서 주석을 보강함.
Project 저장소 구현(SwiftData)
Projects/DVData/Sources/RepositoryImpl/Project/ProjectRepositoryImpl.swift, Projects/DVData/Sources/Storage/Local/Models/Project.swift
ProjectRepositoryImpl이 id/name 중복 검사를 포함한 CRUD를 구현하고, toDomain() 변환을 추가함.
Secret 저장소/검색 변경
Projects/DVData/Sources/RepositoryImpl/Secret/SecretRepositoryImpl.swift, Projects/DVData/Sources/RepositoryImpl/Secret/InMemorySecretQueryFilter.swift, Projects/DVData/Sources/Storage/Local/Models/Secret.swift
secretType/subTyperawValue 기반으로 저장·복원하고, fetchProjects/linkProject/unlinkProject를 구현하며 검색 필터를 정규화함.
Project 유스케이스
Projects/DVDomain/Sources/UseCase/{Error,Impl,Interface}/Project/*.swift
ProjectUseCaseError 매핑과 생성/조회/삭제/이름변경 유스케이스 구현체 및 인터페이스, 이름 정규화 헬퍼를 추가함.
Secret-Project 연결 유스케이스
Projects/DVDomain/Sources/UseCase/Impl/Secret/*.swift, Projects/DVDomain/Sources/UseCase/Interface/Secret/*.swift
Secret 생성 시 프로젝트 연결(실패 시 롤백), patch 시 연결 재조정(reconcileProjects), SecretProjectRelationUseCase의 link/unlink를 추가함.
데모 UI 및 앱 와이어링
Projects/DVPresentation/Sources/ProjectSecretRelationDemoView.swift, Projects/DVPresentation/Sources/SecretUseCaseDemoView.swift, Projects/Devault/Sources/ContentView.swift
프로젝트-시크릿 관계 데모 뷰를 신설하고, 기존 데모 뷰와 앱 진입점에서 새 리포지토리/유스케이스 주입 구조로 교체함.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DemoView as ProjectSecretRelationDemoView
  participant CreateSecretUC as CreateSecretUseCaseImpl
  participant SecretRepo as SecretRepositoryImpl
  participant ProjectRepo as ProjectRepositoryImpl

  DemoView->>CreateSecretUC: execute(draft, payload, projectIDs)
  CreateSecretUC->>SecretRepo: create(secret)
  loop projectIDs
    CreateSecretUC->>SecretRepo: linkProject(secretID, projectID)
    SecretRepo->>ProjectRepo: fetch(id) 존재 확인
  end
  alt 연결 실패
    CreateSecretUC->>SecretRepo: delete(id) 롤백
  else 성공
    CreateSecretUC-->>DemoView: 생성된 Secret 반환
  end
Loading

Possibly related PRs

  • DevaultProject/Devault-macOS#3: 본 PR의 toDomain()ProjectRepositoryImpl CRUD가 참조하는 SwiftData 모델 스키마를 도입한 PR입니다.
  • DevaultProject/Devault-macOS#16: InMemorySecretQueryFilterSecretRepositoryImpl의 SwiftData 기반 시크릿 계층 작업과 직접 맞물려 있습니다.

Suggested reviewers: doyeonk429, yeseonglee

Apple 플랫폼 시니어 개발자 코멘트: Project CRUD와 Secret 연결 로직이 한 PR에 몰려 다소 규모가 크지만, 계층 분리(Domain → Data → UseCase → Presentation)가 명확해 리뷰 동선은 나쁘지 않습니다. createAndLink의 롤백 경로와 reconcileProjects의 차집합 처리 로직을 중점적으로 검증하시고, PatchField 제거된 위치(SecretPatch.swift)가 실제로 다른 파일에 정상 정의됐는지 컴파일 확인을 권장합니다. 🐰

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning PR 템플릿과 데모/문서 주석까지 함께 바뀌어, 이슈의 기능 범위를 넘는 변경이 섞여 있습니다. 관계 기능 검증에 필요한 코드만 남기고, 템플릿·문서·데모 변경은 별도 PR로 분리하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 Project CRUD와 Secret-Project 링크 구현이라는 핵심 변경을 짧고 명확하게 요약합니다.
Linked Issues check ✅ Passed 프로젝트 엔티티, CRUD 저장소/유스케이스, 링크·언링크, 관련 에러와 매핑이 이슈 범위에 맞게 추가되었습니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#25

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
Projects/DVPresentation/Sources/ProjectSecretRelationDemoView.swift (1)

3-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

임포트 순서 수정 필요

내장 프레임워크(SwiftUI)가 먼저, 서드파티/내부 모듈(DVDomain)이 빈 줄로 구분되어 뒤에 와야 합니다. 현재는 순서가 반대이고 구분 줄도 없습니다.

🔧 제안
-import DVDomain
-import SwiftUI
+import SwiftUI
+
+import DVDomain

As per path instructions, "모듈 임포트가 알파벳 순으로 정렬되어 있는지 확인하세요. (내장 프레임워크 먼저, 빈 줄로 서드파티 구분)".

🤖 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/DVPresentation/Sources/ProjectSecretRelationDemoView.swift` around
lines 3 - 4, Adjust the import ordering in ProjectSecretRelationDemoView so
SwiftUI comes first as the built-in framework, then add a blank line before the
internal module import DVDomain. Keep the imports alphabetically ordered within
their groups and preserve the framework-first, separated-by-empty-line
convention.

Source: Path instructions

Projects/Devault/Sources/ContentView.swift (1)

17-38: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

body에서 UseCase를 매번 새로 생성 — init으로 이동 권장

body는 SwiftUI에 의해 재평가될 수 있는 지점입니다. 5개의 UseCase 인스턴스를 여기서 매번 생성하지 말고, init에서 한 번만 구성해 private let으로 보관하는 편이 할당 낭비를 줄이고 의존성 구성 위치도 명확해집니다.

🔧 제안
 struct ContentView: View {
     private let secretRepository: SecretRepositoryImpl
     private let projectRepository: ProjectRepositoryImpl
     private let cryptoService = SecretCryptoServiceImpl()
     private let authenticationService = LocalUserAuthenticationServiceImpl()
+    private let createProjectUseCase: any CreateProjectUseCase
+    private let fetchProjectUseCase: any FetchProjectUseCase
+    private let createSecretUseCase: any CreateSecretUseCase
+    private let fetchSecretUseCase: any FetchSecretUseCase
+    private let secretProjectRelationUseCase: any SecretProjectRelationUseCase

     init(storage: LocalStorage) {
         self.secretRepository = SecretRepositoryImpl(modelContainer: storage.modelContainer)
         self.projectRepository = ProjectRepositoryImpl(modelContainer: storage.modelContainer)
+        self.createProjectUseCase = CreateProjectUseCaseImpl(repository: projectRepository)
+        self.fetchProjectUseCase = FetchProjectUseCaseImpl(repository: projectRepository)
+        self.createSecretUseCase = CreateSecretUseCaseImpl(repository: secretRepository, cryptoService: cryptoService)
+        self.fetchSecretUseCase = FetchSecretUseCaseImpl(repository: secretRepository, cryptoService: cryptoService, authenticationService: authenticationService)
+        self.secretProjectRelationUseCase = SecretProjectRelationUseCaseImpl(repository: secretRepository)
     }

     var body: some View {
         ProjectSecretRelationDemoView(
-            createProjectUseCase: CreateProjectUseCaseImpl(repository: projectRepository),
-            fetchProjectUseCase: FetchProjectUseCaseImpl(repository: projectRepository),
-            createSecretUseCase: CreateSecretUseCaseImpl(repository: secretRepository, cryptoService: cryptoService),
-            fetchSecretUseCase: FetchSecretUseCaseImpl(repository: secretRepository, cryptoService: cryptoService, authenticationService: authenticationService),
-            secretProjectRelationUseCase: SecretProjectRelationUseCaseImpl(repository: secretRepository)
+            createProjectUseCase: createProjectUseCase,
+            fetchProjectUseCase: fetchProjectUseCase,
+            createSecretUseCase: createSecretUseCase,
+            fetchSecretUseCase: fetchSecretUseCase,
+            secretProjectRelationUseCase: secretProjectRelationUseCase
         )
     }
 }
🤖 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/Devault/Sources/ContentView.swift` around lines 17 - 38, body에서
CreateProjectUseCaseImpl, FetchProjectUseCaseImpl, CreateSecretUseCaseImpl,
FetchSecretUseCaseImpl, SecretProjectRelationUseCaseImpl를 매번 새로 만드는 구조를 제거하고,
ContentView의 init에서 한 번만 생성해 private let 프로퍼티로 보관하도록 변경하세요. 그런 다음 body에서는
ProjectSecretRelationDemoView에 미리 준비된 use case들을 전달만 하게 바꿔서, SwiftUI 재평가 시 불필요한
할당이 반복되지 않도록 정리하세요.
Projects/DVDomain/Sources/UseCase/Impl/Secret/PatchSecretUseCaseImpl.swift (1)

29-68: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

update 오버로드 로직 중복 — 공통 헬퍼 추출 권장

metadata 유무만 다를 뿐 payload 암호화 → patch 세팅 → repository.patchreconcileProjects 흐름이 거의 동일합니다. 공통 부분을 private 헬퍼로 추출하면 향후 흐름 변경(예: 에러 매핑, 롤백 로직 추가) 시 한 곳만 수정하면 됩니다.

🤖 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/DVDomain/Sources/UseCase/Impl/Secret/PatchSecretUseCaseImpl.swift`
around lines 29 - 68, The two `update` overloads in `PatchSecretUseCaseImpl`
duplicate the same flow for encrypting payload, building `fullPatch`, calling
`repository.patch`, and then `reconcileProjects`. Extract the shared logic into
a private helper method and have both overloads delegate to it, keeping only the
metadata-specific encoding in the
`update(id:patch:payload:metadata:projectIDs:)` path. This will centralize
future changes in `PatchSecretUseCaseImpl` and reduce the risk of the two code
paths drifting apart.
Projects/DVDomain/Sources/Entity/SecretType.swift (1)

3-42: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

rawValue를 case 이름에 암묵적으로 의존하지 마세요.

SecretType/SecretSubType의 rawValue가 별도 지정 없이 case 이름 그대로 사용되고 있는데, 이 값이 SwiftDataModel.Secret.secretType/subType에 문자열로 직접 영속화됩니다. 추후 case 이름을 리팩터링(오타 수정, 네이밍 개선 등)하면 rawValue도 함께 바뀌어 기존 저장 데이터의 디코딩이 깨질 수 있습니다. 저장되는 값은 case 이름과 별개로 명시적으로 고정해 두는 게 안전합니다.

🛡️ 제안
 public enum SecretType: String, Codable, CaseIterable, Sendable {
-    case apiKeyToken
-    case oauth
-    case database
-    case sshAndCredentials
-    case environmentVariableSet
-    case etc
+    case apiKeyToken = "apiKeyToken"
+    case oauth = "oauth"
+    case database = "database"
+    case sshAndCredentials = "sshAndCredentials"
+    case environmentVariableSet = "environmentVariableSet"
+    case etc = "etc"
🤖 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/DVDomain/Sources/Entity/SecretType.swift` around lines 3 - 42, The
rawValue of SecretType and SecretSubType is currently inferred from case names,
which makes SwiftDataModel.Secret.secretType and subType persistence fragile.
Update the SecretType and SecretSubType enums to use explicit, stable raw string
values instead of relying on implicit case-name defaults. Keep the existing
mapping behavior in availableSubTypes and secretType unchanged, but ensure the
persisted strings remain fixed even if case names are later renamed.
Projects/DVDomain/Sources/UseCase/Impl/Project/FetchProjectUseCaseImpl.swift (1)

12-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

do/catch → map 패턴 중복, 헬퍼로 추출 고려

fetch, fetchAll 둘 다 동일한 do { ... } catch { throw ProjectUseCaseError.map(error) } 골격을 반복합니다. DeleteProjectUseCaseImpl에도 같은 패턴이 있어 Project UseCase 계층 전반에 걸쳐 반복될 가능성이 높습니다. 작은 제네릭 헬퍼로 묶으면 유지보수가 편해집니다.

♻️ 제안: 공통 헬퍼 추출
private func mapped<T: Sendable>(_ operation: () async throws -> T) async throws -> T {
    do {
        return try await operation()
    } catch {
        throw ProjectUseCaseError.map(error)
    }
}

public func fetch(id: UUID) async throws -> Project? {
    try await mapped { try await repository.fetch(id: id) }
}

public func fetchAll() async throws -> [Project] {
    try await mapped { try await repository.fetchAll() }
}
🤖 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/DVDomain/Sources/UseCase/Impl/Project/FetchProjectUseCaseImpl.swift`
around lines 12 - 26, Both FetchProjectUseCaseImpl.fetch(id:) and fetchAll()
repeat the same do/catch mapping to ProjectUseCaseError.map(error), so extract
that logic into a small private generic helper in FetchProjectUseCaseImpl and
have both methods call it. Keep the helper focused on wrapping an async throwing
operation and returning its result, while preserving the existing error mapping
behavior for repository.fetch(id:) and repository.fetchAll().
Projects/DVData/Sources/RepositoryImpl/Project/ProjectRepositoryImpl.swift (2)

3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

임포트 순서가 지침과 반대입니다.

DVDomain(서드파티 모듈)이 Foundation/SwiftData(내장 프레임워크)보다 앞에 있습니다. 내장 프레임워크를 먼저, 빈 줄로 구분 후 서드파티를 배치해야 합니다.

📐 제안 수정
-import DVDomain
 import Foundation
 import SwiftData
+
+import DVDomain

As per path instructions, "모듈 임포트가 알파벳 순으로 정렬되어 있는지 확인하세요. (내장 프레임워크 먼저, 빈 줄로 서드파티 구분)".

🤖 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/RepositoryImpl/Project/ProjectRepositoryImpl.swift`
around lines 3 - 5, The import ordering in ProjectRepositoryImpl is reversed
relative to the module import rule. Reorder the imports so Foundation and
SwiftData appear first, then add a blank line, and place DVDomain after that;
keep the import block alphabetized within each group and use the import list in
ProjectRepositoryImpl as the reference point.

Source: Path instructions


132-144: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

중복 이름 체크가 전체 테이블 스캔.

containsProjectName이 매 create/patch 호출마다 전체 Project를 fetch해서 메모리에서 소문자 비교합니다. SwiftData #Predicate가 대소문자 무관 비교를 기본 지원하지 않아 현재 방식이 실용적인 우회이긴 하나, Project 수가 늘어나면 매 생성/수정마다 O(n) 스캔 비용이 커집니다. 정규화된 이름을 별도 인덱싱 필드로 저장해두면 조회 비용을 줄일 수 있습니다.

🤖 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/RepositoryImpl/Project/ProjectRepositoryImpl.swift`
around lines 132 - 144, containsProjectName is doing a full Project fetch and
in-memory duplicate-name scan on every create/patch, which will not scale.
Update ProjectRepositoryImpl to persist a normalized name field on
SwiftDataModel.Project and keep it in sync in the create/patch flow, then query
by that indexed field instead of fetching all projects; keep the existing
excludedID logic in containsProjectName while moving the lookup to a targeted
fetch/predicate.
🤖 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 @.github/PULL_REQUEST_TEMPLATE.md:
- Around line 2-3: PR 제목 예시가 안내된 컨벤션과 형식이 서로 다릅니다.
.github/PULL_REQUEST_TEMPLATE.md의 PR 제목 예시 문구를 수정해, “타입/#이슈 번호 - 작업 요약” 형식이 실제
예시와 정확히 일치하도록 맞추세요. 특히 예시 문자열에서 Feature 제목의 구분 형식을 하이픈 포함 형태로 바꿔 설명과 동일하게 유지하세요.

In `@Projects/DVData/Sources/Storage/Local/Models/Secret.swift`:
- Around line 83-84: `Secret`의 복원 로직에서 잘못된 `rawValue`를
`DVDomain.SecretType(rawValue:) ?? .etc`와
`DVDomain.SecretSubType.init(rawValue:)`의 fallback으로 숨기지 말고,
`secretType`/`subType`가 enum으로 변환되지 않으면 저장소 손상으로 간주하도록 `Secret` 초기화 또는 매핑 경로를
수정하세요. `DVDomain.SecretType`과 `DVDomain.SecretSubType` 복원 실패 시에는 기본값을 넣지 말고 실패를
전파하거나 별도 오류를 발생시켜, 잘못된 데이터가 정상 도메인 값으로 유입되지 않게 하세요.

In `@Projects/DVDomain/Sources/UseCase/Impl/Secret/CreateSecretUseCaseImpl.swift`:
- Around line 87-95: The rollback in createAndLink(_:projectIDs:) is currently
swallowing delete failures with try?, which can leave the newly created Secret
orphaned after a linkProject failure. Update
CreateSecretUseCaseImpl.createAndLink to either make the create/link/delete
sequence atomic via a repository transaction or explicitly propagate rollback
failure by throwing a distinct error when repository.delete(id:) fails, instead
of ignoring it.
- Around line 90-91: `CreateSecretUseCaseImpl`의 `projectIDs`를 순회하기 전에 중복을 제거하도록
수정하세요. 현재 `for projectID in projectIDs`에서 동일한 Project ID가 두 번 들어오면
`repository.linkProject(secretID:projectID:)`가 중복 링크 에러를 내고 전체 생성이 롤백될 수 있으니, 링크
전에 중복 없는 ID 목록으로 정리한 뒤 그 목록만 사용하도록 `create` 흐름을 바꾸세요.

In `@Projects/DVDomain/Sources/UseCase/Impl/Secret/PatchSecretUseCaseImpl.swift`:
- Around line 72-82: `reconcileProjects(secretID:desiredIDs:)` can leave Secret
metadata updated but project links only partially reconciled if `linkProject` or
`unlinkProject` fails after `repository.patch`; add compensation so the secret
is restored to its previous state or the project changes are rolled back before
rethrowing. Update both `update` overloads in `PatchSecretUseCaseImpl` to treat
patch + reconciliation as one atomic operation at the use-case level, and guard
against stale reads/TOCTOU by reloading or validating the current project set
before applying `fetchProjects`-driven changes.

---

Nitpick comments:
In `@Projects/Devault/Sources/ContentView.swift`:
- Around line 17-38: body에서 CreateProjectUseCaseImpl, FetchProjectUseCaseImpl,
CreateSecretUseCaseImpl, FetchSecretUseCaseImpl,
SecretProjectRelationUseCaseImpl를 매번 새로 만드는 구조를 제거하고, ContentView의 init에서 한 번만
생성해 private let 프로퍼티로 보관하도록 변경하세요. 그런 다음 body에서는 ProjectSecretRelationDemoView에
미리 준비된 use case들을 전달만 하게 바꿔서, SwiftUI 재평가 시 불필요한 할당이 반복되지 않도록 정리하세요.

In `@Projects/DVData/Sources/RepositoryImpl/Project/ProjectRepositoryImpl.swift`:
- Around line 3-5: The import ordering in ProjectRepositoryImpl is reversed
relative to the module import rule. Reorder the imports so Foundation and
SwiftData appear first, then add a blank line, and place DVDomain after that;
keep the import block alphabetized within each group and use the import list in
ProjectRepositoryImpl as the reference point.
- Around line 132-144: containsProjectName is doing a full Project fetch and
in-memory duplicate-name scan on every create/patch, which will not scale.
Update ProjectRepositoryImpl to persist a normalized name field on
SwiftDataModel.Project and keep it in sync in the create/patch flow, then query
by that indexed field instead of fetching all projects; keep the existing
excludedID logic in containsProjectName while moving the lookup to a targeted
fetch/predicate.

In `@Projects/DVDomain/Sources/Entity/SecretType.swift`:
- Around line 3-42: The rawValue of SecretType and SecretSubType is currently
inferred from case names, which makes SwiftDataModel.Secret.secretType and
subType persistence fragile. Update the SecretType and SecretSubType enums to
use explicit, stable raw string values instead of relying on implicit case-name
defaults. Keep the existing mapping behavior in availableSubTypes and secretType
unchanged, but ensure the persisted strings remain fixed even if case names are
later renamed.

In
`@Projects/DVDomain/Sources/UseCase/Impl/Project/FetchProjectUseCaseImpl.swift`:
- Around line 12-26: Both FetchProjectUseCaseImpl.fetch(id:) and fetchAll()
repeat the same do/catch mapping to ProjectUseCaseError.map(error), so extract
that logic into a small private generic helper in FetchProjectUseCaseImpl and
have both methods call it. Keep the helper focused on wrapping an async throwing
operation and returning its result, while preserving the existing error mapping
behavior for repository.fetch(id:) and repository.fetchAll().

In `@Projects/DVDomain/Sources/UseCase/Impl/Secret/PatchSecretUseCaseImpl.swift`:
- Around line 29-68: The two `update` overloads in `PatchSecretUseCaseImpl`
duplicate the same flow for encrypting payload, building `fullPatch`, calling
`repository.patch`, and then `reconcileProjects`. Extract the shared logic into
a private helper method and have both overloads delegate to it, keeping only the
metadata-specific encoding in the
`update(id:patch:payload:metadata:projectIDs:)` path. This will centralize
future changes in `PatchSecretUseCaseImpl` and reduce the risk of the two code
paths drifting apart.

In `@Projects/DVPresentation/Sources/ProjectSecretRelationDemoView.swift`:
- Around line 3-4: Adjust the import ordering in ProjectSecretRelationDemoView
so SwiftUI comes first as the built-in framework, then add a blank line before
the internal module import DVDomain. Keep the imports alphabetically ordered
within their groups and preserve the framework-first, separated-by-empty-line
convention.
🪄 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: c4c9de40-bfba-44fc-b1ba-affc76daf3fa

📥 Commits

Reviewing files that changed from the base of the PR and between 2176f22 and 9f889d8.

📒 Files selected for processing (42)
  • .github/PULL_REQUEST_TEMPLATE.md
  • Projects/DVData/Sources/RepositoryImpl/Project/ProjectRepositoryImpl.swift
  • Projects/DVData/Sources/RepositoryImpl/Secret/InMemorySecretQueryFilter.swift
  • Projects/DVData/Sources/RepositoryImpl/Secret/SecretRepositoryImpl.swift
  • Projects/DVData/Sources/Storage/Local/Models/Project.swift
  • Projects/DVData/Sources/Storage/Local/Models/Secret.swift
  • Projects/DVDomain/Sources/Entity/Project.swift
  • Projects/DVDomain/Sources/Entity/Secret.swift
  • Projects/DVDomain/Sources/Entity/SecretType.swift
  • Projects/DVDomain/Sources/Repository/Error/ProjectRepositoryError.swift
  • Projects/DVDomain/Sources/Repository/Error/SecretRepositoryError.swift
  • Projects/DVDomain/Sources/Repository/Interface/ProjectRepository.swift
  • Projects/DVDomain/Sources/Repository/Interface/SecretRepository.swift
  • Projects/DVDomain/Sources/Repository/PatchField.swift
  • Projects/DVDomain/Sources/Repository/ProjectPatch.swift
  • Projects/DVDomain/Sources/Repository/SecretPatch.swift
  • Projects/DVDomain/Sources/Service/Interface/SecretCryptoService.swift
  • Projects/DVDomain/Sources/Service/Interface/UserAuthenticationService.swift
  • Projects/DVDomain/Sources/UseCase/Draft/SecretDraft.swift
  • Projects/DVDomain/Sources/UseCase/Error/ProjectUseCaseError.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Project/CreateProjectUseCaseImpl.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Project/DeleteProjectUseCaseImpl.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Project/FetchProjectUseCaseImpl.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Project/ProjectUseCaseHelper.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Project/RenameProjectUseCaseImpl.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Secret/CreateSecretUseCaseImpl.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Secret/FetchSecretUseCaseImpl.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Secret/PatchSecretUseCaseImpl.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Secret/SecretProjectRelationUseCaseImpl.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Secret/SecretUseCaseHelper.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Project/CreateProjectUseCase.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Project/DeleteProjectUseCase.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Project/FetchProjectUseCase.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Project/RenameProjectUseCase.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Secret/CreateSecretUseCase.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Secret/DeleteSecretUseCase.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Secret/FetchSecretUseCase.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Secret/PatchSecretUseCase.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Secret/SecretProjectRelationUseCase.swift
  • Projects/DVPresentation/Sources/ProjectSecretRelationDemoView.swift
  • Projects/DVPresentation/Sources/SecretUseCaseDemoView.swift
  • Projects/Devault/Sources/ContentView.swift

Comment thread .github/PULL_REQUEST_TEMPLATE.md Outdated
Comment on lines +83 to +84
secretType: DVDomain.SecretType(rawValue: secretType) ?? .etc,
subType: subType.flatMap(DVDomain.SecretSubType.init(rawValue:)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

잘못된 rawValue를 조용히 .etc/nil로 숨기지 마세요.

저장된 secretType/subType이 enum으로 복원되지 않으면 저장소 손상으로 처리해야 합니다. 지금처럼 fallback하면 잘못된 타입이 정상 데이터처럼 도메인으로 유입됩니다.

🐛 제안 수정
         guard let payload else {
             throw SecretRepositoryError.corruptedStorage
         }
+        guard let domainSecretType = DVDomain.SecretType(rawValue: secretType) else {
+            throw SecretRepositoryError.corruptedStorage
+        }
+        let domainSubType: DVDomain.SecretSubType?
+        if let subType {
+            guard let parsedSubType = DVDomain.SecretSubType(rawValue: subType) else {
+                throw SecretRepositoryError.corruptedStorage
+            }
+            domainSubType = parsedSubType
+        } else {
+            domainSubType = nil
+        }
 
         return DVDomain.Secret(
             id: id,
             name: name,
-            secretType: DVDomain.SecretType(rawValue: secretType) ?? .etc,
-            subType: subType.flatMap(DVDomain.SecretSubType.init(rawValue:)),
+            secretType: domainSecretType,
+            subType: domainSubType,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
secretType: DVDomain.SecretType(rawValue: secretType) ?? .etc,
subType: subType.flatMap(DVDomain.SecretSubType.init(rawValue:)),
guard let payload else {
throw SecretRepositoryError.corruptedStorage
}
guard let domainSecretType = DVDomain.SecretType(rawValue: secretType) else {
throw SecretRepositoryError.corruptedStorage
}
let domainSubType: DVDomain.SecretSubType?
if let subType {
guard let parsedSubType = DVDomain.SecretSubType(rawValue: subType) else {
throw SecretRepositoryError.corruptedStorage
}
domainSubType = parsedSubType
} else {
domainSubType = nil
}
return DVDomain.Secret(
id: id,
name: name,
secretType: domainSecretType,
subType: domainSubType,
🤖 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 83 -
84, `Secret`의 복원 로직에서 잘못된 `rawValue`를 `DVDomain.SecretType(rawValue:) ?? .etc`와
`DVDomain.SecretSubType.init(rawValue:)`의 fallback으로 숨기지 말고,
`secretType`/`subType`가 enum으로 변환되지 않으면 저장소 손상으로 간주하도록 `Secret` 초기화 또는 매핑 경로를
수정하세요. `DVDomain.SecretType`과 `DVDomain.SecretSubType` 복원 실패 시에는 기본값을 넣지 말고 실패를
전파하거나 별도 오류를 발생시켜, 잘못된 데이터가 정상 도메인 값으로 유입되지 않게 하세요.

Comment on lines +87 to +95
func createAndLink(_ secret: Secret, projectIDs: [UUID]) async throws -> Secret {
let created = try await repository.create(secret)
do {
for projectID in projectIDs {
try await repository.linkProject(secretID: created.id, projectID: projectID)
}
} catch {
try? await repository.delete(id: created.id)
throw error

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

rollback 실패를 try?로 숨기지 마세요.

Line 94에서 삭제 실패를 삼키면 link 실패 후 생성된 Secret이 남을 수 있습니다. rollback을 저장소 트랜잭션으로 원자화하거나, rollback 실패를 별도 에러로 표면화하세요.

🤖 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/DVDomain/Sources/UseCase/Impl/Secret/CreateSecretUseCaseImpl.swift`
around lines 87 - 95, The rollback in createAndLink(_:projectIDs:) is currently
swallowing delete failures with try?, which can leave the newly created Secret
orphaned after a linkProject failure. Update
CreateSecretUseCaseImpl.createAndLink to either make the create/link/delete
sequence atomic via a repository transaction or explicitly propagate rollback
failure by throwing a distinct error when repository.delete(id:) fails, instead
of ignoring it.

Comment on lines +90 to +91
for projectID in projectIDs {
try await repository.linkProject(secretID: created.id, projectID: projectID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

projectIDs를 링크 전에 중복 제거하세요.

같은 Project ID가 두 번 들어오면 두 번째 linkProject가 중복 링크 에러를 던져 생성 전체가 rollback됩니다.

🐛 제안 수정
     func createAndLink(_ secret: Secret, projectIDs: [UUID]) async throws -> Secret {
+        var seenProjectIDs = Set<UUID>()
+        let uniqueProjectIDs = projectIDs.filter { seenProjectIDs.insert($0).inserted }
         let created = try await repository.create(secret)
         do {
-            for projectID in projectIDs {
+            for projectID in uniqueProjectIDs {
                 try await repository.linkProject(secretID: created.id, projectID: projectID)
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for projectID in projectIDs {
try await repository.linkProject(secretID: created.id, projectID: projectID)
var seenProjectIDs = Set<UUID>()
let uniqueProjectIDs = projectIDs.filter { seenProjectIDs.insert($0).inserted }
let created = try await repository.create(secret)
do {
for projectID in uniqueProjectIDs {
try await repository.linkProject(secretID: created.id, projectID: projectID)
🤖 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/DVDomain/Sources/UseCase/Impl/Secret/CreateSecretUseCaseImpl.swift`
around lines 90 - 91, `CreateSecretUseCaseImpl`의 `projectIDs`를 순회하기 전에 중복을 제거하도록
수정하세요. 현재 `for projectID in projectIDs`에서 동일한 Project ID가 두 번 들어오면
`repository.linkProject(secretID:projectID:)`가 중복 링크 에러를 내고 전체 생성이 롤백될 수 있으니, 링크
전에 중복 없는 ID 목록으로 정리한 뒤 그 목록만 사용하도록 `create` 흐름을 바꾸세요.

Comment on lines +72 to 82
private extension PatchSecretUseCaseImpl {
func reconcileProjects(secretID: UUID, desiredIDs: [UUID]) async throws {
let currentIDs = Set(try await repository.fetchProjects(secretID: secretID).map(\.id))
let desiredIDs = Set(desiredIDs)
for projectID in desiredIDs.subtracting(currentIDs) {
try await repository.linkProject(secretID: secretID, projectID: projectID)
}
for projectID in currentIDs.subtracting(desiredIDs) {
try await repository.unlinkProject(secretID: secretID, projectID: projectID)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

reconcileProjects 부분 실패 시 데이터 정합성 깨짐 — 롤백/보상 로직 필요

repository.patch가 성공한 뒤 reconcileProjects에서 linkProject/unlinkProject 중 하나라도 실패하면, Secret의 payload/필드는 이미 갱신됐지만 Project 연결은 일부만 반영된 채로 에러가 올라갑니다. 호출자는 전체 실패로 인지하지만 실제로는 부분 성공 상태라 재시도 시 이미 연결된 프로젝트에서 duplicateProjectLink 에러가 재발할 수 있습니다. 또한 fetchProjects(읽기) → link/unlink(쓰기) 사이에 TOCTOU 윈도우가 있어 동일 secret에 대한 동시 patch 호출 시 경합이 발생할 수 있습니다.

update 오버로드(29-46, 48-68) 모두 동일한 위험을 그대로 물려받습니다.

🔧 개선 방향 제안
 private extension PatchSecretUseCaseImpl {
     func reconcileProjects(secretID: UUID, desiredIDs: [UUID]) async throws {
         let currentIDs = Set(try await repository.fetchProjects(secretID: secretID).map(\.id))
         let desiredIDs = Set(desiredIDs)
-        for projectID in desiredIDs.subtracting(currentIDs) {
-            try await repository.linkProject(secretID: secretID, projectID: projectID)
-        }
-        for projectID in currentIDs.subtracting(desiredIDs) {
-            try await repository.unlinkProject(secretID: secretID, projectID: projectID)
-        }
+        var linked: [UUID] = []
+        do {
+            for projectID in desiredIDs.subtracting(currentIDs) {
+                try await repository.linkProject(secretID: secretID, projectID: projectID)
+                linked.append(projectID)
+            }
+            for projectID in currentIDs.subtracting(desiredIDs) {
+                try await repository.unlinkProject(secretID: secretID, projectID: projectID)
+            }
+        } catch {
+            // 실패 시 이번 호출에서 새로 연결한 항목만 되돌려 부분 성공 상태를 방지
+            for projectID in linked {
+                try? await repository.unlinkProject(secretID: secretID, projectID: projectID)
+            }
+            throw error
+        }
     }
 }

이상적으로는 SwiftData modelContext 트랜잭션 수준에서 patch + link/unlink를 하나로 묶는 저장소 API를 노출하는 게 더 견고하지만, 리포지토리 계층 변경 없이 우선 이 정도 보상 로직만이라도 도입하는 걸 권장합니다.

Also applies to: 29-46, 48-68

🤖 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/DVDomain/Sources/UseCase/Impl/Secret/PatchSecretUseCaseImpl.swift`
around lines 72 - 82, `reconcileProjects(secretID:desiredIDs:)` can leave Secret
metadata updated but project links only partially reconciled if `linkProject` or
`unlinkProject` fails after `repository.patch`; add compensation so the secret
is restored to its previous state or the project changes are rolled back before
rethrowing. Update both `update` overloads in `PatchSecretUseCaseImpl` to treat
patch + reconciliation as one atomic operation at the use-case level, and guard
against stale reads/TOCTOU by reloading or validating the current project set
before applying `fetchProjects`-driven changes.

@dlguszoo dlguszoo requested review from doyeonk429 and yeseonglee July 1, 2026 09:06
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: Project CRUD ORM 및 Secret-Project 연결 구현

1 participant