-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/#25 - Project CRUD ORM과 Secret-Project 링크 메서드 구현 #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dlguszoo
wants to merge
24
commits into
develop
Choose a base branch
from
feature/#25
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
f1cfac9
[#25] feat: Project entity 구현
dlguszoo 0558318
[#25] chore: PatchField 파일분리
dlguszoo 02c483e
[#25] feat: ProjectRepository Interface, Error, Patch 구현
dlguszoo 96d336f
[#25] fix: SecretDraft 주석 수정
dlguszoo 78e4b6d
[#25] feat: Project CRUD 관련 UseCase Interface 정의
dlguszoo c23059b
[#25] feat: ProjectUseCase Error 정의, 프로젝트 이름 validation Helper 구현
dlguszoo cde44ab
[#25] feat: Project UseCase Impl 구현
dlguszoo cb49a73
[#25] feat: Project swiftData 모델과 entity 매핑함수 구현
dlguszoo b7254f3
[#25] feat: ProjectRepositoryImpl 구현
dlguszoo 58f8f19
[#25] feat: SecretRepository에 project와 link관련 메서드 정의 및 error 추가
dlguszoo 21848ba
[#25] feat: SecretRepository link 관련 메서드 구체 구현
dlguszoo 412a290
[#25] fix: pr템플릿 수정
dlguszoo 1abefb1
[#25] feat: SecretProjectRelationUseCase 정의 및 구현
dlguszoo 9697994
[#25] feat: 시크릿에 속해 있는 프로젝트 fetch 메서드 정의 및 구현
dlguszoo b26aed1
[#25] fix: 시크릿 patch 통합 메서드로 구현
dlguszoo b172529
[#25] fix: 시크릿 create 통합 메서드로 수정
dlguszoo 70d24d9
[#25] add: usecase/service/repository interface에 각 메서드별 주석 추가
dlguszoo 1883af3
[#25] feat: SecretType, SubType 정의
dlguszoo 49bf713
[#25] fix: Secret, subType 정의에 따른 String 하드 코딩 부분 제거
dlguszoo 9095cda
[#25] feat: Secret type 및 subtype에 따른 secretContent 매핑
dlguszoo 1047867
[#25] delete: presentation layer에서 매핑 처리 결정에 따른 파일 삭제
dlguszoo 1467da8
[#25] fix: type 정의에 따른 하드 코딩 제거
dlguszoo 9f889d8
[#25] test: ProjectSecretRelationDemoView 추가
dlguszoo f12befb
[#25] fix: pr 컨벤션 내용 수정
dlguszoo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
Projects/DVData/Sources/RepositoryImpl/Project/ProjectRepositoryImpl.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| // Copyright © 2026 Devault. All rights reserved | ||
|
|
||
| import DVDomain | ||
| import Foundation | ||
| import SwiftData | ||
|
|
||
| @ModelActor | ||
| public actor ProjectRepositoryImpl: ProjectRepository { | ||
| public func create(_ project: DVDomain.Project) async throws -> DVDomain.Project { | ||
| do { | ||
| if try fetchLocalProject(id: project.id) != nil { | ||
| throw ProjectRepositoryError.duplicateID(id: project.id) | ||
| } | ||
|
|
||
| if try containsProjectName(project.name, excluding: nil) { | ||
| throw ProjectRepositoryError.duplicateName(name: project.name) | ||
| } | ||
|
|
||
| let localProject = SwiftDataModel.Project( | ||
| id: project.id, | ||
| name: project.name, | ||
| createdAt: project.createdAt, | ||
| updatedAt: project.updatedAt | ||
| ) | ||
|
|
||
| modelContext.insert(localProject) | ||
| try modelContext.save() | ||
|
|
||
| return localProject.toDomain() | ||
| } catch let error as ProjectRepositoryError { | ||
| throw error | ||
| } catch { | ||
| throw ProjectRepositoryError.persistenceFailed | ||
| } | ||
| } | ||
|
|
||
| public func fetch(id: UUID) async throws -> DVDomain.Project? { | ||
| do { | ||
| guard let localProject = try fetchLocalProject(id: id) else { | ||
| return nil | ||
| } | ||
| return localProject.toDomain() | ||
| } catch let error as ProjectRepositoryError { | ||
| throw error | ||
| } catch { | ||
| throw ProjectRepositoryError.persistenceFailed | ||
| } | ||
| } | ||
|
|
||
| public func fetchAll() async throws -> [DVDomain.Project] { | ||
| do { | ||
| let descriptor = FetchDescriptor<SwiftDataModel.Project>( | ||
| sortBy: [SortDescriptor(\.name, order: .forward)] | ||
| ) | ||
| let localProjects = try modelContext.fetch(descriptor) | ||
| return localProjects.map { $0.toDomain() } | ||
| } catch let error as ProjectRepositoryError { | ||
| throw error | ||
| } catch { | ||
| throw ProjectRepositoryError.persistenceFailed | ||
| } | ||
| } | ||
|
|
||
| public func patch(id: UUID, with patch: ProjectPatch) async throws -> DVDomain.Project { | ||
| do { | ||
| guard let localProject = try fetchLocalProject(id: id) else { | ||
| throw ProjectRepositoryError.notFound(id: id) | ||
| } | ||
|
|
||
| let shouldSave = try apply(patch, to: localProject) | ||
| if shouldSave { | ||
| try modelContext.save() | ||
| } | ||
|
|
||
| return localProject.toDomain() | ||
| } catch let error as ProjectRepositoryError { | ||
| throw error | ||
| } catch { | ||
| throw ProjectRepositoryError.persistenceFailed | ||
| } | ||
| } | ||
|
|
||
| public func delete(id: UUID) async throws { | ||
| do { | ||
| guard let localProject = try fetchLocalProject(id: id) else { | ||
| throw ProjectRepositoryError.notFound(id: id) | ||
| } | ||
|
|
||
| modelContext.delete(localProject) | ||
| try modelContext.save() | ||
| } catch let error as ProjectRepositoryError { | ||
| throw error | ||
| } catch { | ||
| throw ProjectRepositoryError.persistenceFailed | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension ProjectRepositoryImpl { | ||
| private func fetchLocalProject(id: UUID) throws -> SwiftDataModel.Project? { | ||
| var descriptor = FetchDescriptor<SwiftDataModel.Project>( | ||
| predicate: #Predicate { $0.id == id } | ||
| ) | ||
| descriptor.fetchLimit = 1 | ||
| return try modelContext.fetch(descriptor).first | ||
| } | ||
|
|
||
| /// ProjectPatch를 SwiftData model에 반영하고 저장 필요 여부를 반환한다. | ||
| private func apply( | ||
| _ patch: ProjectPatch, | ||
| to project: SwiftDataModel.Project | ||
| ) throws -> Bool { | ||
| var shouldSave = false | ||
|
|
||
| if case let .set(name) = patch.name { | ||
| if try containsProjectName(name, excluding: project.id) { | ||
| throw ProjectRepositoryError.duplicateName(name: name) | ||
| } | ||
| project.name = name | ||
| shouldSave = true | ||
| } | ||
|
|
||
| if case let .set(updatedAt) = patch.updatedAt { | ||
| project.updatedAt = updatedAt | ||
| shouldSave = true | ||
| } | ||
|
|
||
| return shouldSave | ||
| } | ||
|
|
||
| /// 비교용 이름 key를 기준으로 같은 이름의 Project가 있는지 확인한다. | ||
| private func containsProjectName( | ||
| _ name: String, | ||
| excluding excludedID: UUID? | ||
| ) throws -> Bool { | ||
| let nameKey = projectNameKey(name) | ||
| let descriptor = FetchDescriptor<SwiftDataModel.Project>() | ||
| let projects = try modelContext.fetch(descriptor) | ||
|
|
||
| return projects.contains { project in | ||
| let isExcluded = excludedID.map { project.id == $0 } ?? false // 자기 자신 제외 | ||
| return !isExcluded && projectNameKey(project.name) == nameKey | ||
| } | ||
| } | ||
|
|
||
| private func projectNameKey(_ name: String) -> String { | ||
| name.lowercased() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // Copyright © 2026 Devault. All rights reserved | ||
|
|
||
| import Foundation | ||
|
|
||
| public struct Project: Equatable, Identifiable, Sendable { | ||
| public var id: UUID | ||
| public var name: String | ||
| public var createdAt: Date | ||
| public var updatedAt: Date | ||
|
|
||
| public init( | ||
| id: UUID, | ||
| name: String, | ||
| createdAt: Date, | ||
| updatedAt: Date | ||
| ) { | ||
| self.id = id | ||
| self.name = name | ||
| self.createdAt = createdAt | ||
| self.updatedAt = updatedAt | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
🤖 Prompt for AI Agents