Skip to content

Feature/#77 - 사이드바 Secret 개수 집계 및 표시 - #80

Merged
doyeonk429 merged 6 commits into
developfrom
feature/#77
Aug 8, 2026
Merged

Feature/#77 - 사이드바 Secret 개수 집계 및 표시#80
doyeonk429 merged 6 commits into
developfrom
feature/#77

Conversation

@yeseonglee

@yeseonglee yeseonglee commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✨ 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에 직접 넣어야 목록과 수치가 일치한다.
  • 기존 .expired predicate의 expiresAt! 강제 언래핑을 ?? .distantFuture로 교체. #Predicate 안의 강제 언래핑은 SwiftData가 SQL로 번역하지 못해 fetch 시점에 실패한다.

2. SidebarClient에 fetchCounts 추가

  • 필터 카드 5종 + 프로젝트 N개의 개수를 한 번의 호출로 함께 받아오는 fetchCounts(referenceDate:projectIDs:) 정의.
  • referenceDate는 Live에서 Date()를 직접 부르지 않고 Reducer가 @Dependency(\.date.now)로 주입 — 테스트에서 Expired 기준 시각을 고정할 수 있게 했다.
  • Live 구현은 Projects/Devault/Sources/Composition/Dependencies/SidebarClient+Live.swift에 위치.

3. SidebarFeature 상태·갱신 흐름

  • SecretCounts 모델 신설 (필터별/프로젝트별 개수 묶음, 미조회 항목은 0으로 조회).
  • countsStateLoadingState로 감싸 "아직 안 불러옴"과 "0건"을 구분. View는 counts 옵셔널로 숫자 자리 표시 여부를 판단한다.
  • 프로젝트 목록 로드 성공 후 ID를 넘겨 이어서 집계. 목록 로드가 실패해도 필터 카드 개수는 독립적으로 유효하므로 집계는 계속 시도한다.
  • CancelID.counts + cancelInFlight: true — 생성/삭제가 연달아 일어나면 직전 집계를 취소한다.

4. 개수 무효화 경로 (MainFeature 중재)

  • SecretListFeature가 삭제·복구·영구삭제·프로젝트 연결 시 .delegate(.secretsChanged)를 방출.
  • MainFeature가 이를 받아 .sidebar(.countsRefreshRequested)로 넘긴다. 시크릿 생성 완료 시에도 동일하게 갱신. 자식끼리 직접 연결하지 않고 공통 부모가 중재하는 TCA 가이드라인(7.4)을 따랐다.
  • MainFeatureDate() 직접 호출도 @Dependency(\.date.now)로 교체.

5. 부수 수정

  • DVSecretType 라벨에 lineLimit(1) + fixedSize — 그리드 열이 좁아질 때 줄바꿈으로 행 높이가 연쇄적으로 늘어나던 문제 수정.

🧪 테스트 / 검증 내역

  • SidebarFeatureTests 추가 — counts 로드 성공/실패, 프로젝트 목록 실패 시에도 필터 개수 집계 지속, countsRefreshRequested 재집계 검증
  • MainFeatureTests 추가 — secretsChanged / secretCreatedcountsRefreshRequested 전달 검증
  • SecretListFeatureTests — mutation 성공 시 secretsChanged delegate 방출 검증
  • UI 정상 동작 확인 (사이드바 필터 카드·프로젝트 행 개수 표시, 시크릿 생성/삭제 후 즉시 반영)

💬 기타 공유 사항

  • searchText는 카운트 predicate에 반영하지 않았습니다. 사이드바 개수는 검색어와 무관한 전체 개수를 보여주는 게 맞다고 판단했어요.
  • .notice 필터는 도메인 레이어에 해당 collection이 아직 없어서 .all 쿼리로 임시 매핑되어 있습니다 (기존 TODO 유지). 후속 이슈에서 처리 예정.
  • SecretListFeature.expiringSoonWindowDayspublic으로 열었습니다. 사이드바 Expired 카운트가 목록과 같은 window를 써야 수치가 일치하기 때문입니다.

🙇🏻‍♀️ 리뷰 가이드

  • SecretFetchDescriptorBuilder.swift — 목록 predicate와 카운트 predicate가 왜 갈라져야 하는지(만료 규칙 이중 적용 지점)가 이 PR에서 가장 헷갈릴 부분입니다. 주석으로 근거를 남겼는데 설명이 충분한지 봐주세요.
  • SidebarFeature.swiftprojectsResponse 성공/실패 양쪽에서 countsEffect를 태우는 구조. 실패 시 빈 projectIDs로 진행하는 선택이 적절한지 의견 주시면 좋겠습니다.
  • MainFeature.swift — 자식 간 직접 연결 대신 부모 중재로 개수 갱신을 트리거하는 방식.
  • SidebarClient+Live.swift — 필터 5종 + 프로젝트 N개를 순차 await로 집계합니다. 프로젝트가 많아지면 TaskGroup 병렬화가 필요할 수 있는데, 현재 규모에선 과하다고 판단했습니다.

Summary by CodeRabbit

  • 새 기능

    • 사이드바에 필터와 프로젝트별 비밀 항목 개수를 표시합니다.
    • 비밀 변경, 생성, 삭제, 복구 및 프로젝트 연결 후 개수가 자동으로 갱신됩니다.
    • 개수 조회 실패와 진행 상태를 화면에 반영합니다.
  • 버그 수정

    • 만료일이 없는 항목도 만료 목록 집계에서 안정적으로 처리합니다.
    • 좁은 그리드에서도 비밀 유형 라벨이 줄바꿈되지 않습니다.
  • 개선

    • 기본 창 크기와 최소 크기를 조정했습니다.
    • 상세 영역 표시를 간소화했습니다.

@yeseonglee yeseonglee self-assigned this Aug 8, 2026
@yeseonglee yeseonglee linked an issue Aug 8, 2026 that may be closed by this pull request
1 task
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Secret 개수 조회를 도메인, 저장소, 사이드바 흐름에 연결했습니다. Secret 변경 후 개수를 갱신하고 필터·프로젝트별 개수를 표시합니다. Secret 상세 흐름과 payload 공개 의존성을 제거했습니다. 기본 창과 라벨 표시도 조정했습니다.

Changes

Secret 개수 조회와 저장소 연결

Layer / File(s) Summary
개수 조회 계약과 저장소 구현
Projects/DVData/.../Secret/*, Projects/DVDomain/Sources/Repository/..., Projects/DVDomain/Sources/UseCase/..., Projects/DVDomain/Tests/...
SecretQuery 조건을 기준으로 count API를 추가했습니다. SwiftData fetchCount와 컬렉션별 predicate를 사용합니다.
사이드바 개수 조회와 표시
Projects/DVPresentation/Sources/Dependencies/..., Projects/DVPresentation/Sources/Features/Sidebar/..., Projects/Devault/Sources/Composition/Dependencies/SidebarClient+Live.swift
필터별·프로젝트별 개수 조회, 상태 전환, 요청 취소, 오류 변환을 추가했습니다. 조회 전과 실패 시 개수 라벨을 숨깁니다.
Secret 변경 후 개수 갱신
Projects/DVPresentation/Sources/Features/Main/..., Projects/DVPresentation/Sources/Features/SecretList/..., Projects/DVPresentation/Tests/Main/..., Projects/DVPresentation/Tests/SecretList/...
Secret 변경 이벤트를 전달하고 생성·삭제·복구·영구 삭제·프로젝트 연결 후 개수를 다시 조회합니다. 만료 조회 기준일은 주입된 날짜를 사용합니다.
Secret 상세 흐름 제거
Projects/DVPresentation/Sources/Features/Main/..., Projects/DVPresentation/Sources/Dependencies/SecretClient.swift, Projects/Devault/Sources/Composition/Dependencies/SecretClient+Live.swift
Main의 Secret 상세 상태와 라우팅, payload 공개 의존성 연결을 제거했습니다. 상세 컬럼에는 Detail 텍스트를 표시합니다.
화면 구성 조정
Projects/DVDesign/Sources/Components/..., Projects/Devault/Sources/DevaultApp.swift
Secret 타입 라벨을 한 줄로 표시합니다. 카운트가 없으면 라벨을 표시하지 않습니다. 기본 창 크기를 960×700으로 변경하고 최소 크기를 설정했습니다.

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
Loading

Possibly related PRs

Suggested labels: ✨ Feature, ✅ Test

Suggested reviewers: dlguszoo

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning SecretDetail 기능과 revealPayload 의존성 삭제는 이슈 #77의 개수 집계 요구와 직접 관련이 없는 범위 확장입니다. SecretDetail 및 revealPayload 삭제를 별도 PR로 분리하거나, 이슈 #77에 해당 변경의 명확한 요구사항과 근거를 추가하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% 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 제목은 사이드바의 Secret 개수 집계 및 표시라는 PR의 핵심 변경을 직접 설명합니다.
Linked Issues check ✅ Passed 이슈 #77의 sidebar 데이터 연결과 Secret 개수 fetching 요구를 구현과 테스트로 충족합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#77

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

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 37f52cf and d717d25.

📒 Files selected for processing (18)
  • Projects/DVData/Sources/RepositoryImpl/Secret/SecretFetchDescriptorBuilder.swift
  • Projects/DVData/Sources/RepositoryImpl/Secret/SecretRepositoryImpl.swift
  • Projects/DVDesign/Sources/Components/DVSecretType.swift
  • Projects/DVDomain/Sources/Repository/Interface/SecretRepository.swift
  • Projects/DVDomain/Sources/UseCase/Impl/Secret/FetchSecretUseCaseImpl.swift
  • Projects/DVDomain/Sources/UseCase/Interface/Secret/FetchSecretUseCase.swift
  • Projects/DVDomain/Tests/Core/Support/InMemorySecretRepository.swift
  • Projects/DVPresentation/Sources/Dependencies/SidebarClient.swift
  • Projects/DVPresentation/Sources/Features/Main/MainFeature.swift
  • Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift
  • Projects/DVPresentation/Sources/Features/Sidebar/Model/SecretCounts.swift
  • Projects/DVPresentation/Sources/Features/Sidebar/SidebarFeature.swift
  • Projects/DVPresentation/Sources/Features/Sidebar/SidebarView.swift
  • Projects/DVPresentation/Tests/Main/MainFeatureTests.swift
  • Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift
  • Projects/DVPresentation/Tests/Sidebar/SidebarFeatureTests.swift
  • Projects/Devault/Sources/Composition/Dependencies/SidebarClient+Live.swift
  • Projects/Devault/Sources/DevaultApp.swift

Comment on lines +47 to +60
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)

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 | 🟠 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.liked predicate에 SecretQuerysecretType, 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.

Comment on lines +52 to +60
/// 그리드 열이 좁아져도 라벨이 줄바꿈되지 않도록 고정한다.
/// 줄바꿈을 허용하면 행 높이가 커지면서 그리드 전체 높이가 연쇄적으로 늘어난다.
private var typeLabel: some View {
Text(labelText)
.dvFont(.headingLG)
.foregroundStyle(Color.dv(.gray900))
.multilineTextAlignment(.center)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)

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

🧩 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 || true

Repository: 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 || true

Repository: DevaultProject/Devault-macOS

Length of output: 25145


typeLabel의 가로 fixedSize를 줄바꿈 대신 truncation으로 처리하세요.

SelectSecretTypeViewLazyVGrid(columns: [...GridItem(.flexible())...])처럼 유연한 열 너비를 제공하지만, DVSecretTypetypeLabellineLimit(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).

Comment thread Projects/DVDomain/Tests/Core/Support/InMemorySecretRepository.swift Outdated
Comment thread Projects/DVPresentation/Sources/Features/Main/MainFeature.swift
Comment on lines 161 to +165
case .mutationResponse(.success):
return fetchSecretsEffect(query: state.query, debounced: false)
return .merge(
fetchSecretsEffect(query: state.query, debounced: false),
.send(.delegate(.secretsChanged))
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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.swift

Repository: 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.swift

Repository: 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:


🌐 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:


🌐 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:


🌐 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:


🌐 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:


🌐 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:


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-L182
  • Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift#L177-L177
  • Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift#L216-L216
  • Projects/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행 테스트는 두 액션의
특정 도착 순서를 요구하지 않도록 수정하세요.

yeseonglee and others added 5 commits August 8, 2026 16:24
count 경로의 .all/.liked predicate가 secretType·service·environment를
무시해 fetch 결과와 계약이 갈리던 문제를 수정한다. 테스트 저장소의
count도 전체 개수 대신 같은 규칙으로 판정하도록 구현해, 필터별 카운트가
모두 같은 값이어도 테스트가 통과하던 구멍을 막는다.

목록 재조회와 부모 갱신 delegate를 .merge로 묶으면 도착 순서가
보장되지 않아 테스트가 깨지기 쉬우므로 .concatenate로 바꾼다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@doyeonk429 doyeonk429 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  1. 로딩·실패 상태가 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 표시만은 피하는 게 좋겠습니다.

이거 로딩안되면 아예 카운트 표시를 안하는건 어떨까?

@doyeonk429

Copy link
Copy Markdown
Contributor

@yeseonglee
4. 30일 window가 두 레이어에 중복 구현돼 있습니다

같은 규칙이 두 곳에 있습니다.

  • 목록: SecretListFeature.State.query (referenceDate를 30일 밀어서 .expired 쿼리 생성)
  • 카운트: SidebarClient+Live.swift:111 SidebarFilter.expired.collection(referenceDate:)

지금은 값이 일치해서 결과가 맞습니다(확인함). 하지만 한쪽만 바뀌면 조용히 어긋나고, 어긋나도 테스트가 못 잡습니다. expiringSoonWindowDays를 public으로 연 것만으로는 부족하고, "referenceDate → expired 쿼리 collection"을 만드는 함수 자체를 한 곳에 두고 양쪽이 호출하는 게 안전합니다.

이것도 util로 빼서 계산 가능하면 좋을 듯!

@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: 1

🧹 Nitpick comments (1)
Projects/DVPresentation/Sources/Features/Main/MainFeature.swift (1)

52-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

nowprivate로 제한하세요.

nowMainFeature와 같은 파일의 extension MainFeature에서만 사용됩니다. 현재 기본 internal 접근 범위는 불필요하게 넓습니다.

접근 범위 축소 예시
-  `@Dependency`(\.date.now) var now
+  `@Dependency`(\.date.now) private var now

As 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2931c3c and 6393539.

📒 Files selected for processing (11)
  • Projects/DVPresentation/Resources/Localizable.xcstrings
  • Projects/DVPresentation/Sources/Dependencies/SecretClient.swift
  • Projects/DVPresentation/Sources/Features/Main/MainFeature.swift
  • Projects/DVPresentation/Sources/Features/Main/MainView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailError.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailFeature.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailView.swift
  • Projects/DVPresentation/Sources/Features/Sidebar/SidebarFeature.swift
  • Projects/DVPresentation/Tests/Main/MainFeatureTests.swift
  • Projects/DVPresentation/Tests/SecretDetail/SecretDetailFeatureTests.swift
  • Projects/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

Comment on lines +101 to +105
// `.merge`는 도착 순서를 보장하지 않아 테스트가 깨지기 쉬우므로 순차 실행한다.
return .concatenate(
.send(.sidebar(.setCreatingSecret(false))),
.send(.sidebar(.countsRefreshRequested))
)

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 | 🟠 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/Tests

Repository: 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)
PY

Repository: 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}")
PY

Repository: 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.

@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.

🧹 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로 변경하고, 공개가 필요한 Collectioncollection public 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6393539 and 0000329.

📒 Files selected for processing (8)
  • Projects/DVDesign/Sources/Components/DVCategory.swift
  • Projects/DVDesign/Sources/Components/DVProjectContainer.swift
  • Projects/DVDomain/Sources/Repository/SecretQuery.swift
  • Projects/DVDomain/Tests/Core/Repository/SecretQueryTests.swift
  • Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift
  • Projects/DVPresentation/Sources/Features/Sidebar/SidebarView.swift
  • Projects/DVPresentation/Tests/SecretList/SecretListFeatureTests.swift
  • Projects/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>
@doyeonk429
doyeonk429 self-requested a review August 8, 2026 09:45
@doyeonk429
doyeonk429 merged commit d428d52 into develop Aug 8, 2026
1 check passed
@doyeonk429
doyeonk429 deleted the feature/#77 branch August 8, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: sidebar data 연결

3 participants