Feature/#77 - 사이드바 Secret 개수 집계 및 표시 - #80
Conversation
WalkthroughSecret 개수 조회를 도메인, 저장소, 사이드바 흐름에 연결했습니다. Secret 변경 후 개수를 갱신하고 필터·프로젝트별 개수를 표시합니다. Secret 상세 흐름과 payload 공개 의존성을 제거했습니다. 기본 창과 라벨 표시도 조정했습니다. ChangesSecret 개수 조회와 저장소 연결
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SecretListFeature
participant MainFeature
participant SidebarFeature
participant SidebarClient
SecretListFeature->>MainFeature: delegate(secretsChanged)
MainFeature->>SidebarFeature: countsRefreshRequested
SidebarFeature->>SidebarClient: fetchCounts(referenceDate, projectIDs)
SidebarClient-->>SidebarFeature: SecretCounts
SidebarFeature-->>SidebarFeature: countsResponse
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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/RepositoryImpl/Secret/SecretFetchDescriptorBuilder.swift`:
- Around line 47-60: Update SecretFetchDescriptorBuilder.swift lines 47-60 so
the .all and .liked predicates apply SecretQuery.secretType, service, and
environment filters consistently with fetch(_:) while preserving deletion,
expiration, and liked conditions. Update InMemorySecretRepository.swift lines
73-77 so count(_:) evaluates the supplied query instead of returning all
secrets. Add contract tests associated with SecretRepository.swift lines 21-26
to verify every SecretRepository implementation returns counts matching the
query filters.
In `@Projects/DVDesign/Sources/Components/DVSecretType.swift`:
- Around line 52-60: Update DVSecretType.typeLabel to respect the flexible grid
column width by removing the horizontal fixedSize behavior and applying tail
truncation to the single-line label. Preserve the existing styling, centered
alignment, and lineLimit(1).
In `@Projects/DVDomain/Tests/Core/Support/InMemorySecretRepository.swift`:
- Around line 73-76: In `count(_:)`, stop returning `secrets.count`
unconditionally and apply the `SecretQuery.Collection` and all additional
filters using the same rules as `SecretRepositoryImpl.count(_:)`. Include
deleted, favorite, expired, and project-association criteria while preserving
the query counter and injected error behavior.
In `@Projects/DVPresentation/Sources/Features/Main/MainFeature.swift`:
- Line 186: Align the expired-list reference date in MainFeature with the
sidebar count by applying the same expiringSoonWindowDays window used by
SidebarClient+Live. Prefer reusing a shared filter-to-collection mapping if one
exists, so both paths classify expiring Secrets consistently.
In `@Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift`:
- Around line 161-165: SecretListFeature의 mutationResponse 성공 처리와 관련 테스트에서
secretsChanged와 목록 응답의 도착 순서 의존성을 제거하세요. SecretListFeature.swift의 161-165행과
177-182행에서는 부모 갱신을 먼저 시작해야 한다면 fetchSecretsEffect와 delegate 전송을 순차 실행하도록 변경하고,
그렇지 않으면 병렬 실행을 유지하세요. SecretListFeatureTests.swift의 177행, 216행, 234행 테스트는 두 액션의
특정 도착 순서를 요구하지 않도록 수정하세요.
🪄 Autofix
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: 43b8a6ec-36cc-43bc-aca7-81b5586af77a
📒 Files selected for processing (18)
Projects/DVData/Sources/RepositoryImpl/Secret/SecretFetchDescriptorBuilder.swiftProjects/DVData/Sources/RepositoryImpl/Secret/SecretRepositoryImpl.swiftProjects/DVDesign/Sources/Components/DVSecretType.swiftProjects/DVDomain/Sources/Repository/Interface/SecretRepository.swiftProjects/DVDomain/Sources/UseCase/Impl/Secret/FetchSecretUseCaseImpl.swiftProjects/DVDomain/Sources/UseCase/Interface/Secret/FetchSecretUseCase.swiftProjects/DVDomain/Tests/Core/Support/InMemorySecretRepository.swiftProjects/DVPresentation/Sources/Dependencies/SidebarClient.swiftProjects/DVPresentation/Sources/Features/Main/MainFeature.swiftProjects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swiftProjects/DVPresentation/Sources/Features/Sidebar/Model/SecretCounts.swiftProjects/DVPresentation/Sources/Features/Sidebar/SidebarFeature.swiftProjects/DVPresentation/Sources/Features/Sidebar/SidebarView.swiftProjects/DVPresentation/Tests/Main/MainFeatureTests.swiftProjects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swiftProjects/DVPresentation/Tests/Sidebar/SidebarFeatureTests.swiftProjects/Devault/Sources/Composition/Dependencies/SidebarClient+Live.swiftProjects/Devault/Sources/DevaultApp.swift
| switch query.collection { | ||
| case .all: | ||
| return #Predicate<SwiftDataModel.Secret> { secret in | ||
| secret.deletedAt == nil && | ||
| (secret.expiresAt ?? neverExpires) >= referenceDate | ||
| } | ||
| case .liked: | ||
| return #Predicate<SwiftDataModel.Secret> { secret in | ||
| secret.deletedAt == nil && | ||
| secret.liked && | ||
| (secret.expiresAt ?? neverExpires) >= referenceDate | ||
| } | ||
| case .expired, .deleted, .project: | ||
| return predicate(from: query) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
개수 조회에 모든 구조화 필터를 적용하세요.
Line 49와 Line 54의 predicate는 secretType, service, environment를 무시합니다. 반면 목록 조회는 같은 필터를 적용합니다. 따라서 count(_:) 결과가 fetch(_:) 결과와 달라집니다.
제공된 Projects/DVDomain/Tests/Core/Support/InMemorySecretRepository.swift:73-77도 쿼리를 무시하고 전체 secrets.count를 반환합니다. 테스트 저장소도 같은 쿼리 의미를 구현하세요.
Projects/DVData/Sources/RepositoryImpl/Secret/SecretFetchDescriptorBuilder.swift#L47-L60:.all및.likedpredicate에SecretQuery의secretType,service,environment조건을 추가하세요.Projects/DVDomain/Sources/Repository/Interface/SecretRepository.swift#L21-L26: 모든SecretRepository구현체가count(_:)에서 쿼리 조건을 적용하도록 계약 테스트를 추가하세요.
📍 Affects 2 files
Projects/DVData/Sources/RepositoryImpl/Secret/SecretFetchDescriptorBuilder.swift#L47-L60(this comment)Projects/DVDomain/Sources/Repository/Interface/SecretRepository.swift#L21-L26
🤖 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/Secret/SecretFetchDescriptorBuilder.swift`
around lines 47 - 60, Update SecretFetchDescriptorBuilder.swift lines 47-60 so
the .all and .liked predicates apply SecretQuery.secretType, service, and
environment filters consistently with fetch(_:) while preserving deletion,
expiration, and liked conditions. Update InMemorySecretRepository.swift lines
73-77 so count(_:) evaluates the supplied query instead of returning all
secrets. Add contract tests associated with SecretRepository.swift lines 21-26
to verify every SecretRepository implementation returns counts matching the
query filters.
| /// 그리드 열이 좁아져도 라벨이 줄바꿈되지 않도록 고정한다. | ||
| /// 줄바꿈을 허용하면 행 높이가 커지면서 그리드 전체 높이가 연쇄적으로 늘어난다. | ||
| private var typeLabel: some View { | ||
| Text(labelText) | ||
| .dvFont(.headingLG) | ||
| .foregroundStyle(Color.dv(.gray900)) | ||
| .multilineTextAlignment(.center) | ||
| .lineLimit(1) | ||
| .fixedSize(horizontal: true, vertical: false) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate files =="
git ls-files | rg 'DVSecretType|SelectSecretType|SecretType' || true
echo "== file sizes =="
for f in $(git ls-files | rg 'Projects/DVDesign/Sources/(Components/DVSecretType|.*SelectSecretType|.*SecretType).*\.swift' || true); do
wc -l "$f"
done
echo "== DVSecretType outline and relevant contents =="
if [ -f Projects/DVDesign/Sources/Components/DVSecretType.swift ]; then
ast-grep outline Projects/DVDesign/Sources/Components/DVSecretType.swift || true
sed -n '1,140p' Projects/DVDesign/Sources/Components/DVSecretType.swift
fi
echo "== SelectSecretType relevant references =="
rg -n "SelectSecretTypeView|typeLabel|labelText|type\.displayName|secret type|SecretType" Projects/DVDesign/Sources -g '*.swift' -C 3 || trueRepository: DevaultProject/Devault-macOS
Length of output: 5611
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== SelectSecretTypeView relevant contents =="
sed -n '1,180p' Projects/DVPresentation/Sources/Features/SelectSecretType/SelectSecretTypeView.swift
echo "== DVSecretType preview usage =="
sed -n '1,140p' Projects/DVDesign/SampleApp/Sources/DVSecretTypePreviewView.swift
echo "== SecretType definitions =="
for f in Projects/DVDomain/Sources/Entity/SecretType.swift Projects/DVPresentation/Sources/Features/CreateSecret/Model/CreatableSecretType.swift; do
echo "--- $f"
sed -n '1,220p' "$f"
done
echo "== grid modifiers / DVSecretType usages =="
rg -n "DVSecretType|AnyGridItem|CollectionGrid|LazyVGrid|horizontalSizeClass|Fixed|gridColumn|lineLimit|fixedSize|truncationMode|\.center" Projects/DVPresentation Projects/DVDesign -g '*.swift' -C 2 || true
echo "== deterministic SwiftUI modifier order behavior probe =="
cat > /tmp/swiftui_fixedsize_order_probe.swift <<'SWIFT'
struct TextExtensions {
static func lineLimit(_ value: Int? = 0) {}
static func fixedSize(horizontal: Bool = true, vertical: Bool = false) {}
static func truncationMode(_ mode: String = "tail") {}
}
// This is a source-only compile probe, not the repository code.
// A Text would accept lineLimit → truncationMode → fixedSize;
// lineLimit without truncationMode and with fixedSize often preserves intrinsic label width.
SWIFT
swiftc --version 2>/dev/null || trueRepository: DevaultProject/Devault-macOS
Length of output: 25145
typeLabel의 가로 fixedSize를 줄바꿈 대신 truncation으로 처리하세요.
SelectSecretTypeView가 LazyVGrid(columns: [...GridItem(.flexible())...])처럼 유연한 열 너비를 제공하지만, DVSecretType의 typeLabel은 lineLimit(1) 위에 fixedSize(horizontal: true, vertical: false)를 적용해 부모 제안 너비를 무시합니다. 긴 현지화 라벨은 카드 너비를 벗어나고, 가로 고정과 truncationMode(.tail)은 동시에 적용할 수 있으므로 제거 방향을 고려하세요.
🤖 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/DVDesign/Sources/Components/DVSecretType.swift` around lines 52 -
60, Update DVSecretType.typeLabel to respect the flexible grid column width by
removing the horizontal fixedSize behavior and applying tail truncation to the
single-line label. Preserve the existing styling, centered alignment, and
lineLimit(1).
| case .mutationResponse(.success): | ||
| return fetchSecretsEffect(query: state.query, debounced: false) | ||
| return .merge( | ||
| fetchSecretsEffect(query: state.query, debounced: false), | ||
| .send(.delegate(.secretsChanged)) | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
fd -a '^(Package\.swift|Package\.resolved)$' . -0 \
| xargs -0 -r rg -n -C 2 'swift-composable-architecture|ComposableArchitecture'
rg -n -C 5 '\.merge\(|\.concatenate\(' \
Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift \
Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swiftRepository: DevaultProject/Devault-macOS
Length of output: 2928
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '--- SecretListFeature.swift outline ---\n'
ast-grep outline Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift \
--match SecretListFeature --view expanded || true
printf '\n--- SecretListFeature.swift relevant source ---\n'
sed -n '1,240p' Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift | nl -ba
printf '\n--- SecretListFeatureTests.swift outline/relevant tests ---\n'
ast-grep outline Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift \
--match SecretListFeatureTests --view expanded || true
rg -n -C 8 'secretsChanged|test.*Mutation|mutationResponse|projectLinked|receive' Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swiftRepository: DevaultProject/Devault-macOS
Length of output: 257
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- SecretListFeature.swift relevant source ---'
sed -n '1,240p' Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift | nl -ba
printf '%s\n' '--- SecretListFeatureTests.swift relevant tests ---'
rg -n -C 10 'secretsChanged|mutationResponse|projectLinked|receive' Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift
printf '%s\n' '--- Package manifest TCA dependency ---'
fd -a '^(Package\.swift|Package\.resolved)$' . -0 \
| xargs -0 -r rg -n -C 3 'swift-composable-architecture|ComposableArchitecture'Repository: DevaultProject/Devault-macOS
Length of output: 254
🌐 Web query:
Swift Composable Architecture 1.26 Effect.merge runs effects concurrently order of actions delivery
💡 Result:
In the Composable Architecture (TCA), Effect.merge runs effects concurrently [1][2][3]. When you use Effect.merge, all provided effects are executed at the same time [1][3]. Because these effects run concurrently, the order in which they finish and dispatch their respective actions to the store is non-deterministic and depends entirely on the internal implementation of those effects (e.g., network latency, processing time, or async scheduling) [4][5]. TCA does not guarantee the order of action delivery for merged effects [5]. If your application logic relies on a specific sequence of operations or actions, Effect.merge is not the appropriate tool [6]. Instead, you should use Effect.concatenate, which ensures that effects are executed and completed one after the other, respecting the order in which they are provided [2][7][6]. For complex scenarios where you need to manage multiple concurrent tasks, TCA's use of Structured Concurrency (specifically withTaskGroup under the hood for Effect.run) means that while the start times are simultaneous, the final order of actions arriving at the store remains subject to the completion time of each individual asynchronous task [1].
Citations:
- 1: https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Effect.swift
- 2: https://forums.swift.org/t/is-there-a-way-to-compose-effects/58597
- 3: https://pointfreeco-swift-composable-architecture.mintlify.app/api/effect
- 4: Concatenated Effects - response ordering pointfreeco/swift-composable-architecture#547
- 5: https://forums.swift.org/t/testing-receiving-actions-of-child-features/75686
- 6: https://forums.swift.org/t/action-splitting/36664
- 7: https://forums.swift.org/t/strange-behaviour-of-nested-concatenate-s/44486
🌐 Web query:
https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Effect.swift merge
💡 Result:
In the Composable Architecture (TCA), Effect.merge is a utility function used to combine multiple effects into a single effect that executes them concurrently [1][2][3]. Key characteristics of Effect.merge include: - Concurrent Execution: When effects are merged, they are initiated and run at the same time [1][2][3]. - Implementation: It utilizes Swift's structured concurrency, specifically using withTaskGroup under the hood to manage the concurrent execution of these tasks [4]. - API Evolution: While early versions of TCA relied heavily on Combine-based publishers for effects, the library has moved toward Swift concurrency (async/await). Although the maintainers have at times discussed reducing the library's surface area by deprecating such utilities in favor of native task groups within Effect.run, Effect.merge has been retained as a convenient utility in the 2.0+ versions of the architecture [5][6]. Usage patterns typically involve passing a variadic list or a sequence of effects to the merge function: Effect.merge(effect1, effect2, effect3) This is frequently used in reducers when a single action needs to trigger multiple independent side effects simultaneously [3]. For scenarios requiring sequential execution, the library provides Effect.concatenate [3][7].
Citations:
- 1: https://swiftinit.org/docs/swift-composable-architecture/composablearchitecture/effect.merge(_:)?hash=5AI73
- 2: https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/1.23.1/documentation/composablearchitecture/effect/merge(with:)
- 3: https://forums.swift.org/t/action-splitting/36664
- 4: Effect.merge vs withTaskGroup pointfreeco/swift-composable-architecture#2469
- 5: Trait-deprecate
Effect.concatenate,Effect.mappointfreeco/swift-composable-architecture#3879 - 6: Merge send/task/run/fireForget/merge/concat into a single API name pointfreeco/swift-composable-architecture#2046
- 7: https://forums.swift.org/t/queueing-effects-and-knowing-when-theyre-all-completed/37055
🌐 Web query:
Github pointfreeco swift-composable-architecture 1.26 Sources ComposableArchitecture Effect.swift merge
💡 Result:
In the Swift Composable Architecture (TCA) library, including version 1.26, the Effect.merge function is used to combine multiple side effects into a single effect that executes them concurrently [1][2][3]. The Effect.merge implementation in Sources/ComposableArchitecture/Effect.swift provides two primary static methods for composing effects [1][2]: 1. Variadic version: public static func merge(_ effects: Self...) -> Self This allows you to pass a variadic list of effects directly, which are then merged together [1][2]. 2. Sequence version: public static func merge(_ effects: some Sequence) -> Self This accepts a sequence of effects and uses a reduction to merge them [1][4]. Internally, the merge(with:) instance method defines how two effects are combined based on their underlying operations [1]: - If one effect is .none, it returns the other [1]. - If both are based on Combine publishers, it uses Publishers.Merge to combine them [1]. - If both are .run (Swift concurrency) operations, it uses a withTaskGroup to execute the child tasks concurrently [1]. While TCA has deprecated or removed certain effect-transforming operators (such as .concatenate and .map in some contexts) to align with Swift concurrency best practices, the maintainers have explicitly retained .merge as a utility [5]. It remains the recommended way to run multiple focused effects at the same time [3].
Citations:
- 1: https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Effect.swift
- 2: https://pointfreeco-swift-composable-architecture.mintlify.app/api/effect
- 3: https://mintlify.wiki/pointfreeco/swift-composable-architecture/concepts/effects
- 4: pointfreeco/swift-composable-architecture@bc8f27b
- 5: Trait-deprecate
Effect.concatenate,Effect.mappointfreeco/swift-composable-architecture#3879
🌐 Web query:
Composable Architecture Effect.send debounced run concurrency fetchSecretsEffect
💡 Result:
In the Composable Architecture (TCA), there are two primary approaches for debouncing side effects like fetchSecretsEffect. 1. Manual Debouncing with Task.sleep (Recommended) Modern TCA favors Swift Concurrency, and the most common pattern for debouncing is to perform a manual sleep within an Effect.run block and use the.cancellable modifier [1][2][3]. This gives you explicit control over cancellation [3]. Example pattern: case let.searchQueryChanged(query): return.run { send in try await Task.sleep(for:.milliseconds(300)) let secrets = try await fetchSecretsEffect(query) await send(.secretsLoaded(secrets)) }.cancellable(id: CancelID.search, cancelInFlight: true) In this approach, the.cancellable(id:cancelInFlight:true) modifier ensures that if a new action with the same ID arrives before the previous task finishes, the previous one is canceled [3]. 2. Legacy.debounce Operator TCA historically provided a dedicated.debounce operator for effects [4][5]. However, this operator has been largely superseded by the manual pattern described above [3]. The.debounce operator was designed to debounce based on the actions sent by the effect rather than the effect's execution itself [6]. Regarding Concurrency: - Effects created with.run execute in the Swift cooperative thread pool [7][8]. - Sending actions back to the system via the provided send function automatically hops to the MainActor [7][8]. - Use @Sendable closures and ensure all dependencies are Sendable to comply with Swift Concurrency requirements [7][8]. Avoid using Effect.send for logic sharing; it is intended for emitting actions [9][1][2]. Use helper functions for shared logic instead [1][2].
Citations:
- 1: https://www.mintlify.com/pointfreeco/swift-composable-architecture/concepts/effects
- 2: https://pointfreeco-swift-composable-architecture.mintlify.app/concepts/effects
- 3: A Little Help With Debouncing? pointfreeco/swift-composable-architecture#2197
- 4: https://github.com/pointfreeco/swift-composable-architecture/blob/main/Tests/ComposableArchitectureTests/EffectDebounceTests.swift
- 5: .debounce doesn't seem to work as expected pointfreeco/swift-composable-architecture#919
- 6: add
timeoutthat can be cancel pointfreeco/swift-composable-architecture#1082 - 7: https://www.mintlify.com/pointfreeco/swift-composable-architecture/guides/concurrency
- 8: https://pointfreeco-swift-composable-architecture.mintlify.app/guides/concurrency
- 9: https://pointfreeco-swift-composable-architecture.mintlify.app/api/effect
🌐 Web query:
github pointfreeco swift-composable-architecture 1.26 Effect.send debounced run
💡 Result:
In the Composable Architecture (TCA), the previous Effect.debounce(id:for:scheduler:) operator has been deprecated in favor of a modern approach using structured concurrency [1][2]. Instead of using the deprecated .debounce operator, the recommended way to implement debouncing in TCA (as of version 1.25+) is to use Task.sleep combined with cancellable logic within an Effect.run block [1][2]. For example, to debounce a search query, you should now structure your effect as follows [3][4]: case let.searchQueryChanged(query): state.searchQuery = query return.run { send in try await Task.sleep(for:.milliseconds(300)) await send(.performSearch) }.cancellable(id: CancelID.search, cancelInFlight: true) The .cancellable modifier on the effect ensures that if a new action with the same ID (e.g., CancelID.search) is emitted, any in-flight effect associated with that ID is automatically cancelled [1][2]. This replaces the behavior of the old debounce operator by using the cancelInFlight: true parameter, which mimics the debouncing effect by cancelling pending work when a new action triggers the same effect ID [1][2]. Note that Effect.send is intended for emitting actions into the system, but it is not the primary way to perform complex, asynchronous, or debounced logic [3][5]. That logic is handled by Effect.run, which provides the send function to emit actions after your asynchronous work (like Task.sleep) has completed [3][6]. Top results used for this answer: [3][1][4][2]
Citations:
- 1: https://github.com/pointfreeco/swift-composable-architecture/blob/1.25.5/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.25.md
- 2: https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.25.md
- 3: https://pointfreeco-swift-composable-architecture.mintlify.app/concepts/effects
- 4: https://mintlify.wiki/pointfreeco/swift-composable-architecture/concepts/effects
- 5: https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Effect.swift
- 6: https://pointfreeco-swift-composable-architecture.mintlify.app/api/effect
🌐 Web query:
github pointfreeco swift-composable-architecture 1.26 Effect.send debounced fetchSecretsEffect implementation
💡 Result:
In the Composable Architecture (TCA), debouncing side effects has evolved away from chaining operators toward more idiomatic async/await patterns [1]. While you may see examples referencing .debounce(id:for:scheduler:), the library has moved to favor the .task(id:) view modifier for view-driven debouncing or manual Task.sleep within an .run effect [1][2][3]. If you are implementing a debounced effect (such as a hypothetical fetchSecretsEffect), you should no longer rely on operator-based debouncing for standard async work [1]. Instead, use the following recommended patterns: 1. View-Driven Debouncing (Recommended) If the debounce is triggered by a view state change (e.g., text field input), handle it in the view layer using the.task(id:) modifier [4][2]. This automatically cancels the previous task when the ID changes [4]..task(id: store.searchQuery) { do { try await Task.sleep(for:.milliseconds(300)) await store.send(.fetchSecrets).finish } catch {} } 2. Reducer-Driven Debouncing If you must implement this within the reducer, use an explicit sleep inside an.run effect and ensure the effect is cancellable [2][3]. case.textChanged(let query): state.query = query return.run { send in try await Task.sleep(for:.milliseconds(300)) await send(.fetchSecrets) }.cancellable(id: CancelID.fetchSecrets) Key Implementation Details: - Cancellation: Always use .cancellable(id:) to manage the lifetime of the effect [2][3]. This ensures that if a new action triggers the same effect, the previous in-flight request is cancelled [5][4]. - Migration: The Effect.debounce operator is deprecated in favor of these explicit async/await patterns [1]. - Effect.send: Note that Effect.send is intended for immediately emitting a single action and is generally not used for shared logic or debounced chains [6][3]. For complex async work, .run is the standard tool [6][3].
Citations:
- 1: https://github.com/pointfreeco/swift-composable-architecture/blob/1.25.5/Sources/ComposableArchitecture/Documentation.docc/Articles/MigrationGuides/MigratingTo1.25.md
- 2: A Little Help With Debouncing? pointfreeco/swift-composable-architecture#2197
- 3: https://www.mintlify.com/pointfreeco/swift-composable-architecture/concepts/effects
- 4: https://mintlify.wiki/pointfreeco/swift-composable-architecture/examples/search
- 5: https://github.com/pointfreeco/swift-composable-architecture/blob/1.25.5/Examples/Search/README.md
- 6: https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Effect.swift
secretsChanged와 목록 응답의 순서 의존성을 제거하세요.
Effect.merge 실행은 비동기 작업의 결과 순서를 보장하지 않습니다. 현재 테스트는 mutationResponse 또는 destination 응답보다 먼저 secretsChanged를 항상 도달한다고 가정하므로 불안정해질 수 있습니다.
- 부모 갱신 먼저 시작이 필요하면
.concatenate(.send(.delegate(.secretsChanged)), fetchSecretsEffect(...))를 사용하세요. - 병렬 실행이 의도라면 테스트에서 두 액션의 도착 순서를 고정하지 마세요.
📍 Affects 2 files
Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift#L161-L165(this comment)Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift#L177-L182Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift#L177-L177Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift#L216-L216Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift#L234-L234
🤖 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/Features/SecretList/SecretListFeature.swift`
around lines 161 - 165, SecretListFeature의 mutationResponse 성공 처리와 관련 테스트에서
secretsChanged와 목록 응답의 도착 순서 의존성을 제거하세요. SecretListFeature.swift의 161-165행과
177-182행에서는 부모 갱신을 먼저 시작해야 한다면 fetchSecretsEffect와 delegate 전송을 순차 실행하도록 변경하고,
그렇지 않으면 병렬 실행을 유지하세요. SecretListFeatureTests.swift의 177행, 216행, 234행 테스트는 두 액션의
특정 도착 순서를 요구하지 않도록 수정하세요.
count 경로의 .all/.liked predicate가 secretType·service·environment를 무시해 fetch 결과와 계약이 갈리던 문제를 수정한다. 테스트 저장소의 count도 전체 개수 대신 같은 규칙으로 판정하도록 구현해, 필터별 카운트가 모두 같은 값이어도 테스트가 통과하던 구멍을 막는다. 목록 재조회와 부모 갱신 delegate를 .merge로 묶으면 도착 순서가 보장되지 않아 테스트가 깨지기 쉬우므로 .concatenate로 바꾼다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
doyeonk429
left a comment
There was a problem hiding this comment.
- 로딩·실패 상태가 View에서 0으로 뭉개집니다 (중요)
SidebarView.swift:210
private func count(for filter: SidebarFilter) -> Int {
store.counts?.count(for: filter) ?? 0
}
State에서 LoadingState로 "로드 전"과 "0건"을 애써 구분해놓고, 유일한 소비 지점에서 ?? 0으로 되돌립니다. 결과적으로:
- 앱 시작 직후 모든 카드가 잠깐 0을 보여줬다가 실제 숫자로 바뀝니다 (깜빡임)
- 집계 실패 시 에러 표시 없이 영구히 0 — 사용자는 "시크릿이 없다"로 읽습니다. 조용히 틀린 데이터를 보여주는 게 제일 나쁜 실패 모드입니다
PR 본문에는 "View는 counts 옵셔널로 숫자 자리 표시 여부를 판단한다"고 쓰여 있는데 코드는 그렇지 않습니다. 지금 상태로는 LoadingState 래핑이 실질적으로 얻는 게 없습니다.
DVCategory(count:)가 non-optional Int라 고치려면 컴포넌트를 Int?로 열거나 별도 표현(–, 스켈레톤)이 필요합니다. 비용이 있는 건 맞지만, 최소한 실패 시 0 표시만은 피하는 게 좋겠습니다.
이거 로딩안되면 아예 카운트 표시를 안하는건 어떨까?
|
@yeseonglee 같은 규칙이 두 곳에 있습니다.
지금은 값이 일치해서 결과가 맞습니다(확인함). 하지만 한쪽만 바뀌면 조용히 어긋나고, 어긋나도 테스트가 못 잡습니다. expiringSoonWindowDays를 public으로 연 것만으로는 부족하고, "referenceDate → expired 쿼리 collection"을 만드는 함수 자체를 한 곳에 두고 양쪽이 호출하는 게 안전합니다.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
Projects/DVPresentation/Sources/Features/Main/MainFeature.swift (1)
52-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
now를private로 제한하세요.
now는MainFeature와 같은 파일의extension MainFeature에서만 사용됩니다. 현재 기본internal접근 범위는 불필요하게 넓습니다.접근 범위 축소 예시
- `@Dependency`(\.date.now) var now + `@Dependency`(\.date.now) private var nowAs per path instructions: 접근 제어가 가능한 가장 엄격한 수준인지 확인하세요. (
private>fileprivate>internal)🤖 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/Features/Main/MainFeature.swift` around lines 52 - 55, Change the now dependency declaration in MainFeature to private access, since it is only used within MainFeature and its same-file extension; do not broaden the scope to fileprivate or internal.Source: Path instructions
🤖 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/DVPresentation/Sources/Features/Main/MainFeature.swift`:
- Around line 101-105: Update the `.secretCreated` handling to trigger a Secret
list refetch after creation succeeds, such as dispatching `.secretList(.task)`
or the established `fetchSecretsEffect` action. Preserve the existing sequential
sidebar state reset and count refresh, and ensure the refetch runs as part of
the same completion flow.
---
Nitpick comments:
In `@Projects/DVPresentation/Sources/Features/Main/MainFeature.swift`:
- Around line 52-55: Change the now dependency declaration in MainFeature to
private access, since it is only used within MainFeature and its same-file
extension; do not broaden the scope to fileprivate or internal.
🪄 Autofix
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: 946860d2-e797-4869-ad47-0c1c1010695c
📒 Files selected for processing (11)
Projects/DVPresentation/Resources/Localizable.xcstringsProjects/DVPresentation/Sources/Dependencies/SecretClient.swiftProjects/DVPresentation/Sources/Features/Main/MainFeature.swiftProjects/DVPresentation/Sources/Features/Main/MainView.swiftProjects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailError.swiftProjects/DVPresentation/Sources/Features/SecretDetail/SecretDetailFeature.swiftProjects/DVPresentation/Sources/Features/SecretDetail/SecretDetailView.swiftProjects/DVPresentation/Sources/Features/Sidebar/SidebarFeature.swiftProjects/DVPresentation/Tests/Main/MainFeatureTests.swiftProjects/DVPresentation/Tests/SecretDetail/SecretDetailFeatureTests.swiftProjects/Devault/Sources/Composition/Dependencies/SecretClient+Live.swift
💤 Files with no reviewable changes (4)
- Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailError.swift
- Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailView.swift
- Projects/DVPresentation/Tests/SecretDetail/SecretDetailFeatureTests.swift
- Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailFeature.swift
🚧 Files skipped from review as they are similar to previous changes (2)
- Projects/DVPresentation/Tests/Main/MainFeatureTests.swift
- Projects/DVPresentation/Sources/Features/Sidebar/SidebarFeature.swift
| // `.merge`는 도착 순서를 보장하지 않아 테스트가 깨지기 쉬우므로 순차 실행한다. | ||
| return .concatenate( | ||
| .send(.sidebar(.setCreatingSecret(false))), | ||
| .send(.sidebar(.countsRefreshRequested)) | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 8 \
'secretCreated|\.secretList\(\.task\)|SecretListFeature\.Action\.task|createSecret' \
Projects/DVPresentation/Sources \
Projects/DVPresentation/TestsRepository: DevaultProject/Devault-macOS
Length of output: 43229
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- MainView outline / relevant sections ---\n'
ast-grep outline Projects/DVPresentation/Sources/Features/Main/MainView.swift || true
sed -n '1,140p' Projects/DVPresentation/Sources/Features/Main/MainView.swift
printf '\n--- SecretList files ---\n'
fd -a 'SecretList.*\.swift' Projects/DVPresentation/Sources | sed 's#^\./##'
for f in $(fd 'SecretList.*\.swift' Projects/DVPresentation/Sources); do
echo "### $f"
wc -l "$f"
ast-grep outline "$f" || true
done
printf '\n--- MainFeature reducer secretList handling ---\n'
sed -n '1,180p' Projects/DVPresentation/Sources/Features/Main/MainFeature.swift
printf '\n--- ProjectCreated related code ---\n'
rg -n -C 6 'projectCreated|selectSecretType\.delegate|secretsChanged|Task\.id|\.task|refreshRequested|countsRefreshRequested' Projects/DVPresentation/Sources/Features Projects/DVPresentation/Tests/Features ProjectTests 2>/dev/null || true
printf '\n--- Behavioral source-data model check ---\n'
python3 - <<'PY'
from pathlib import Path
import re
root = Path('Projects/DVPresentation/Sources')
files = sorted(root.rglob('*.swift'))
needle = r'\.secretList\(\.task\)|task\(.*SecretList|secretsChanged|countsRefreshRequested|makeSecretListState'
need = ['secretCreated', 'Task', 'secretsChanged', 'Task\.id', 'countsRefreshRequested']
targets = {'Main': [False, False], 'SecretListTask': False, 'SecretListMutationReload': False}
for p in files:
text = p.read_text(errors='ignore')
if 'SecretListFeature' in text or any(s in text for s in needle.split('|')):
rel = str(p)
for n in need:
if n in text:
targets[n] = True
if 'projectCreated' in text:
print('projectCreated reference:', rel)
if 'secretCreated' in text:
print('secretCreated reference:', rel)
print(targets)
PYRepository: DevaultProject/Devault-macOS
Length of output: 257
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- MainView outline / relevant sections ---'
ast-grep outline Projects/DVPresentation/Sources/Features/Main/MainView.swift || true
sed -n '1,140p' Projects/DVPresentation/Sources/Features/Main/MainView.swift
printf '%s\n' ''
printf '%s\n' '--- SecretList files ---'
fd -a 'SecretList.*\.swift' Projects/DVPresentation/Sources | sed 's#^\./##'
for f in $(fd 'SecretList.*\.swift' Projects/DVPresentation/Sources); do
printf '### %s\n' "$f"
wc -l "$f"
ast-grep outline "$f" || true
done
printf '%s\n' ''
printf '%s\n' '--- MainFeature reducer secretList handling ---'
sed -n '1,180p' Projects/DVPresentation/Sources/Features/Main/MainFeature.swift
printf '%s\n' ''
printf '%s\n' '--- ProjectCreated related code ---'
rg -n -C 6 'projectCreated|selectSecretType\.delegate|secretsChanged|Task\.id|\.task|refreshRequested|countsRefreshRequested' Projects/DVPresentation/Sources/Features Projects/DVPresentation/Tests/Features 2>/dev/null || true
printf '%s\n' ''
printf '%s\n' '--- Behavioral source-data model check ---'
python3 - <<'PY'
from pathlib import Path
root = Path('Projects/DVPresentation/Sources')
files = sorted(root.rglob('*.swift'))
targets = {
"secretCreated_reference": False,
"secretList_task_reference": False,
"secretsChanged_reference": False,
"countsRefreshRequested_reference": False,
}
for p in files:
text = p.read_text(errors='ignore')
if 'secretCreated' in text:
targets["secretCreated_reference"] = True
if '.secretList(.task)' in text or 'Task' in text and 'id' in text:
targets["secretList_task_reference"] = True
if 'secretsChanged' in text:
targets["secretsChanged_reference"] = True
if 'countsRefreshRequested' in text:
targets["countsRefreshRequested_reference"] = True
print(targets)
for target, present in targets.items():
if not present:
print(f"MISSING: {target}")
PYRepository: DevaultProject/Devault-macOS
Length of output: 50384
생성 완료 후 Secret 목록도 다시 읽어오도록 갱신하세요.
.secretCreated handling에서 사이드바 개수만 갱신하고 현재 목록 상태를 재조회하지 않습니다. 생성 성공 후 .secretList(.task)나 fetchSecretsEffect와 같은 재조회 액션을 실행하지 않으면 새 Secret이 목록에 표시되지 않을 수 있습니다.
🤖 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/Features/Main/MainFeature.swift` around lines
101 - 105, Update the `.secretCreated` handling to trigger a Secret list refetch
after creation succeeds, such as dispatching `.secretList(.task)` or the
established `fetchSecretsEffect` action. Preserve the existing sequential
sidebar state reset and count refresh, and ensure the refetch runs as part of
the same completion flow.
There was a problem hiding this comment.
🧹 Nitpick comments (3)
Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift (1)
171-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win프로젝트 연결 성공 경로를 테스트하세요.
Line 171-177에서
.projectLinked성공 시delegate(.secretsChanged)와 목록 재조회를 추가했습니다. 현재Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift에는 삭제, 복구, 영구 삭제 성공 테스트만 있습니다..projectLinked액션을 보내고delegate(.secretsChanged)가secretsResponse보다 먼저 도착하는지 검증하는TestStore테스트를 추가하세요. 이 테스트가 사이드바 카운트 갱신 계약과 순차 실행을 보호합니다.🤖 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/Features/SecretList/SecretListFeature.swift` around lines 171 - 177, Add a TestStore case in SecretListFeatureTests covering the .destination(.presented(.addToProject(.delegate(.projectLinked)))) success path. Send the projectLinked action, assert delegate(.secretsChanged) is received before the secretsResponse from the refresh effect, and provide the required response so the sequential fetch behavior is verified.Projects/DVDomain/Tests/Core/Repository/SecretQueryTests.swift (1)
42-56: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win실제 predicate 경계를 검증하세요.
현재 테스트는 날짜와
windowEnd의 대소 관계만 확인합니다. 실제 저장소 predicate를 실행하지 않으므로expiresAt == windowEnd가 제외되는지 검증하지 못합니다.
windowEnd직전, 정확히windowEnd, 직후의 Secret을 실제 조회에 넣고 결과를 확인하도록 테스트를 보강하세요.🤖 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/Tests/Core/Repository/SecretQueryTests.swift` around lines 42 - 56, 보강된 테스트가 실제 저장소 predicate를 실행하도록 `expiringWindowCoversPastAndUpcoming`을 수정하세요. `windowEnd` 직전·정확히 일치·직후의 `Secret`을 저장소에 넣고 조회한 뒤, 직전 항목만 결과에 포함되고 `expiresAt == windowEnd` 및 직후 항목은 제외되는지 검증하세요.Projects/DVDomain/Sources/Repository/SecretQuery.swift (1)
38-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
public extension SecretQuery선언을 선언별 접근 제어 패턴으로 변경하세요.
extension SecretQuery로 변경하고, 공개가 필요한Collection과collectionpublic var,expiringSoonWindowDays,expiringWindow(from:)에만 접근 제어를 직접 명시하세요.🤖 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/Repository/SecretQuery.swift` around lines 38 - 54, Update the public extension SecretQuery declaration to a non-public extension, then explicitly mark only Collection, collection, expiringSoonWindowDays, and expiringWindow(from:) as public. Leave all other declarations using default access control.Source: Path instructions
🤖 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.
Nitpick comments:
In `@Projects/DVDomain/Sources/Repository/SecretQuery.swift`:
- Around line 38-54: Update the public extension SecretQuery declaration to a
non-public extension, then explicitly mark only Collection, collection,
expiringSoonWindowDays, and expiringWindow(from:) as public. Leave all other
declarations using default access control.
In `@Projects/DVDomain/Tests/Core/Repository/SecretQueryTests.swift`:
- Around line 42-56: 보강된 테스트가 실제 저장소 predicate를 실행하도록
`expiringWindowCoversPastAndUpcoming`을 수정하세요. `windowEnd` 직전·정확히 일치·직후의
`Secret`을 저장소에 넣고 조회한 뒤, 직전 항목만 결과에 포함되고 `expiresAt == windowEnd` 및 직후 항목은 제외되는지
검증하세요.
In `@Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift`:
- Around line 171-177: Add a TestStore case in SecretListFeatureTests covering
the .destination(.presented(.addToProject(.delegate(.projectLinked)))) success
path. Send the projectLinked action, assert delegate(.secretsChanged) is
received before the secretsResponse from the refresh effect, and provide the
required response so the sequential fetch behavior is verified.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4d97231b-2a8b-4793-b140-fdd1f2501748
📒 Files selected for processing (8)
Projects/DVDesign/Sources/Components/DVCategory.swiftProjects/DVDesign/Sources/Components/DVProjectContainer.swiftProjects/DVDomain/Sources/Repository/SecretQuery.swiftProjects/DVDomain/Tests/Core/Repository/SecretQueryTests.swiftProjects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swiftProjects/DVPresentation/Sources/Features/Sidebar/SidebarView.swiftProjects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swiftProjects/Devault/Sources/Composition/Dependencies/SidebarClient+Live.swift
🚧 Files skipped from review as they are similar to previous changes (2)
- Projects/DVPresentation/Sources/Features/Sidebar/SidebarView.swift
- Projects/Devault/Sources/Composition/Dependencies/SidebarClient+Live.swift
State에서 LoadingState로 "로드 전"과 "0건"을 구분해놓고 View에서 `?? 0`으로 되돌리고 있었다. 집계에 실패하면 에러 표시 없이 0이 남아 "시크릿 없음"으로 읽히므로, DVCategory·DVProjectContainer의 count를 옵셔널로 열고 nil이면 숫자 자리를 비운다. 만료 window(referenceDate + 30일) 계산이 목록과 사이드바 카운트에 각각 복붙돼 있어 한쪽만 바뀌면 조용히 어긋났다. SecretQuery.Collection에 expiringWindow(from:)을 두고 양쪽이 호출하게 한다. 상수를 위해 열어뒀던 SecretListFeature.expiringSoonWindowDays의 public도 걷어낸다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
✨ What’s this PR?
📌 관련 이슈 (Related Issue)
🧶 주요 변경 내용 (Summary)
1. 개수 집계 전용 경로 신설 (Domain → Data)
SecretRepository.count(_:)/FetchSecretUseCase.count(query:)추가 — 목록 본문이 필요 없는 사이드바 카운트를 위해fetch(_:).count대신fetchCount로 개수만 센다. 엔티티를 메모리로 올리지 않아 가볍고, 손상된 레코드가 섞여 있어도 집계는 실패하지 않는다.SecretFetchDescriptorBuilder.makeCountDescriptor(from:referenceDate:)추가.fetch경로는 predicate 통과 후InMemorySecretQueryFilter가 만료 항목을 한 번 더 걸러내므로, 개수 경로에서는 그 만료 규칙을 predicate에 직접 넣어야 목록과 수치가 일치한다..expiredpredicate의expiresAt!강제 언래핑을?? .distantFuture로 교체.#Predicate안의 강제 언래핑은 SwiftData가 SQL로 번역하지 못해 fetch 시점에 실패한다.2. SidebarClient에
fetchCounts추가fetchCounts(referenceDate:projectIDs:)정의.referenceDate는 Live에서Date()를 직접 부르지 않고 Reducer가@Dependency(\.date.now)로 주입 — 테스트에서 Expired 기준 시각을 고정할 수 있게 했다.Projects/Devault/Sources/Composition/Dependencies/SidebarClient+Live.swift에 위치.3. SidebarFeature 상태·갱신 흐름
SecretCounts모델 신설 (필터별/프로젝트별 개수 묶음, 미조회 항목은 0으로 조회).countsState를LoadingState로 감싸 "아직 안 불러옴"과 "0건"을 구분. View는counts옵셔널로 숫자 자리 표시 여부를 판단한다.CancelID.counts+cancelInFlight: true— 생성/삭제가 연달아 일어나면 직전 집계를 취소한다.4. 개수 무효화 경로 (MainFeature 중재)
SecretListFeature가 삭제·복구·영구삭제·프로젝트 연결 시.delegate(.secretsChanged)를 방출.MainFeature가 이를 받아.sidebar(.countsRefreshRequested)로 넘긴다. 시크릿 생성 완료 시에도 동일하게 갱신. 자식끼리 직접 연결하지 않고 공통 부모가 중재하는 TCA 가이드라인(7.4)을 따랐다.MainFeature의Date()직접 호출도@Dependency(\.date.now)로 교체.5. 부수 수정
DVSecretType라벨에lineLimit(1)+fixedSize— 그리드 열이 좁아질 때 줄바꿈으로 행 높이가 연쇄적으로 늘어나던 문제 수정.🧪 테스트 / 검증 내역
SidebarFeatureTests추가 — counts 로드 성공/실패, 프로젝트 목록 실패 시에도 필터 개수 집계 지속,countsRefreshRequested재집계 검증MainFeatureTests추가 —secretsChanged/secretCreated→countsRefreshRequested전달 검증SecretListFeatureTests— mutation 성공 시secretsChangeddelegate 방출 검증💬 기타 공유 사항
searchText는 카운트 predicate에 반영하지 않았습니다. 사이드바 개수는 검색어와 무관한 전체 개수를 보여주는 게 맞다고 판단했어요..notice필터는 도메인 레이어에 해당 collection이 아직 없어서.all쿼리로 임시 매핑되어 있습니다 (기존 TODO 유지). 후속 이슈에서 처리 예정.SecretListFeature.expiringSoonWindowDays를public으로 열었습니다. 사이드바 Expired 카운트가 목록과 같은 window를 써야 수치가 일치하기 때문입니다.🙇🏻♀️ 리뷰 가이드
SecretFetchDescriptorBuilder.swift— 목록 predicate와 카운트 predicate가 왜 갈라져야 하는지(만료 규칙 이중 적용 지점)가 이 PR에서 가장 헷갈릴 부분입니다. 주석으로 근거를 남겼는데 설명이 충분한지 봐주세요.SidebarFeature.swift—projectsResponse성공/실패 양쪽에서countsEffect를 태우는 구조. 실패 시 빈projectIDs로 진행하는 선택이 적절한지 의견 주시면 좋겠습니다.MainFeature.swift— 자식 간 직접 연결 대신 부모 중재로 개수 갱신을 트리거하는 방식.SidebarClient+Live.swift— 필터 5종 + 프로젝트 N개를 순차await로 집계합니다. 프로젝트가 많아지면TaskGroup병렬화가 필요할 수 있는데, 현재 규모에선 과하다고 판단했습니다.Summary by CodeRabbit
새 기능
버그 수정
개선