Skip to content

Feature/#75 - 시크릿 조회 화면 9개 타입 구현 및 가변 폼 레이아웃 도입 - #82

Open
doyeonk429 wants to merge 34 commits into
developfrom
feature/#75
Open

Feature/#75 - 시크릿 조회 화면 9개 타입 구현 및 가변 폼 레이아웃 도입#82
doyeonk429 wants to merge 34 commits into
developfrom
feature/#75

Conversation

@doyeonk429

@doyeonk429 doyeonk429 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

✨ What’s this PR?

📌 관련 이슈 (Related Issue)


🧶 주요 변경 내용 (Summary)

1. 컴포넌트 폭 정책 도입 (DVDesign)

  • DVComponentWidthPolicy(.fixed / .fill)를 Environment로 추가하고 dvComponentWidth(_:height:alignment:) 헬퍼로 7개 입력 컴포넌트에 적용
  • 기본값이 .fixed라 기존 호출부는 한 줄도 바뀌지 않습니다. 폭이 늘어야 하는 화면에서만 .fill을 주입
  • DVMultiSelectDropdown의 팝오버 패널은 의도적으로 고정 폭 유지 — 트리거만 늘어나는 게 macOS에서 자연스럽습니다
  • DVRadioButton 읽기 전용 모드, DVChipsContainer(read-only chip), DVMultilineTextContainer 추가

2. 폼 레이아웃을 "화면 맥락 × 배열" 두 계층으로 분리

기존 단일 FormLayoutMode로는 생성 화면(단일 컬럼)과 조회 화면(3컬럼 중 detail 컬럼)의 요구가 충돌했습니다.

  • FormLayoutContext(protocol, 화면 맥락) × FormLayout(컨테이너 폭에서 파생된 실제 배열)로 분리
  • StandaloneFormLayout(생성 화면) / DetailColumnFormLayout(조회 컬럼) 두 구현
  • 임계값 816pt = md(380) × 2 + 간격 16 + 좌우 패딩 40. 이보다 넓어지면 xs 듀얼 → md 듀얼로 승격
  • 생성·조회 본문 모두 최대 폭 816으로 제한. 창을 키웠을 때 footer가 화면 끝까지 밀려나 조작이 불편했던 문제가 해소됩니다. header/footer는 본문과 분리해 프레임을 그대로 따릅니다

3. 조회 전용 read-only 컴포넌트

  • DetailReadOnlyFieldView, DetailProjectFieldView, SecretDetailHeaderView, DetailSectionScaffoldView
  • 원칙: 생성 화면이 실제로 쓰는 컴포넌트의 read-only 대응만 만듭니다. 생성 화면에 없는 표현 형태는 새로 도입하지 않습니다
  • 개행이 포함된 값(PEM, JSON, KEY=value 목록)은 DVMultilineTextContainer로 자동 전환. 기존 lineLimit(1)에서는 첫 줄만 보여 값을 읽을 수 없었습니다

4. 타입별 payload 조회 섹션 9종 + 디스패처

  • Detail{APIKeysToken, OAuthClient, ServiceAccount, Database, SSHKey, SSLTLSCert, EnvSet, LicenseKey, Custom}SectionView
  • DetailPayloadSectionView가 11개 (secretType, subType) 조합을 default 없이 분기 — 타입이 늘면 컴파일 에러로 잡힙니다
  • 공통 메타 행(Project/Services/Expire Date/Environment)은 생성 화면 각 섹션의 실제 입력 필드를 측정해 타입별로 맞췄습니다. 9개 중 4개만 동일한 구성이라 스캐폴드에 하드코딩하지 않고 Name·Memo로 축소했습니다

5. 기능 구현

  • 즐겨찾기 / 삭제 / 연결 프로젝트 조회 (SecretClientsetLiked·fetchLinkedProjects·revealPayload 추가)
  • payload 복호화는 Touch ID를 거치며, 실패 시 재시도 액션 제공. 복호화 전에는 필드 대신 상태 뷰를 렌더
  • 즐겨찾기·저장·삭제 후 목록 재조회 — 항목 교체만으로는 liked/expired/project 같은 필터 컬렉션에서 조건을 벗어난 항목이 남습니다

6. 만료 임박 표기를 목록·조회 통일

  • SecretExpiryStatus(정책: 이미 만료 또는 3일 이내 → .critical, 7일 이내 → .upcoming) / DVExpiryEmphasis(표현: 아이콘·색)로 레이어 분리
  • 이미 만료된 건을 3일 경고와 합쳤습니다 — 사용자가 취해야 할 조치가 같습니다(추후 QA 후 수정 가능)
  • 목록도 같은 소스를 쓰도록 옮겨 두 화면이 서로 다르게 판정할 수 없게 했습니다. 경계(3일·7일 포함 여부)를 고정하는 테스트 11개 추가

7. 라우팅 버그 수정

  • 생성 플로우 진입 시 조회 중이던 secretDetail·목록 선택을 해제. 생성 중에는 2컬럼이라 상세가 화면에서 사라지지만 State는 살아남아, 생성을 마치고 3컬럼으로 돌아오면 이전 상세가 되살아나고 Touch ID를 다시 요구했습니다
  • 완료·취소 어느 경로로 나가도 stale 상태가 남지 않도록 진입 시점에서 끊었습니다

🧪 테스트 / 검증 내역

  • develop 리베이스 후 재검증 (develop..HEAD 32커밋, HEAD..develop 0)
  • DVDesign · DVPresentation · Devault(Debug) BUILD SUCCEEDED
  • 유닛 테스트 125개 / 13 suite 통과
  • 9개 타입 × 11개 subType 조합 프리뷰 작성 (좁은 폭 / 듀얼 폭 variant 포함)
  • 가변 폭 동작 확인 — 생성·조회 화면에서 폭을 늘였을 때 듀얼 승격 및 최대 폭 제한
  • 생성 → 조회 왕복 시나리오 수동 확인 — 진행 중 (Touch ID 문제로 지연, Debug 메뉴로 우회 후 진행)
  • Release 구성 빌드 — 미검증 (아래 기타 공유 사항 참고)

💬 기타 공유 사항

  • Release 구성이 develop 기준으로 이미 깨져 있습니다. PreviewSupport/PreviewWidth.swift가 파일 전체 #if DEBUG인데 CreateSecretHeaderView·FooterActionsView 등의 #Preview 블록은 감싸이지 않아 Release에서 previewWidth를 찾지 못합니다. 이 PR에서 수정한 파일이 아니라 손대지 않았고, 별도 이슈로 정리가 필요합니다
  • generate-local 빌드에서 Touch ID가 실패합니다. ad-hoc 서명이라 온보딩·잠금을 통과할 수 없어, 화면 검증용으로 #if DEBUG Debug 메뉴(⌥⌘M)를 임시로 넣었습니다. #64로 팀 시트가 해소되면 제거하도록 TODO를 걸어두었습니다. 원인 확정은 LocalUserAuthenticationServiceImpl이 실제 LAError를 버리고 있어 로그 추가가 필요하며, DVData 변경이라 이 PR 범위에서 제외했습니다
  • 생성 화면과 조회 화면의 여러 줄 입력이 비대칭입니다. 조회는 개행을 모두 보여주지만 생성은 credentialJSON·envContent도 여전히 한 줄 입력입니다. 파리티 원칙과 어긋나는 방향이라 별도 판단이 필요합니다
  • SSL/TLS 인증서의 notAfter 파싱(SecCertificateCopyValues)은 이 PR 범위 밖으로 두고 별도 이슈로 분리했습니다
  • Touch ID 인증 세션 캐싱이 없어 revealPayload마다 인증이 뜹니다 — 의도된 현재 상태이고 별도 작업 예정입니다
  • SecretListFeature.swift:152,155의 미사용 반환값 경고는 develop에서 softDelete/restoreSecret을 반환하도록 바뀐 데서 오는 선행 경고입니다

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

대부분 생성 -> 조회 동작에서 이상 없는지 확인하면 충분함. 기타 이상해보이는 동작은, 노션에 메모해두면 추후 QA 기간 때 수정 예정

  • Form/FormLayout.swift, Form/FormLayoutContext.swift — 맥락 × 배열 2계층 설계와 816pt 임계값 산출
  • Foundations/Sizing/DVComponentWidthPolicy.swift — 기본값 .fixed로 기존 호출부에 영향이 없는지
  • SecretDetail/Components/Sections/DetailPayloadSectionView.swift — 11개 조합 분기, 타입별 공통 메타 행 구성
  • Support/SecretExpiryStatus.swift + Foundations/Expiry/DVExpiryEmphasis.swift — 정책/표현 분리 경계
  • SecretDetail/Components/DetailReadOnlyFieldView.swift — 개행 감지 후 멀티라인 전환, 빈 값일 때 복사 버튼 숨김
  • Main/MainFeature.swift — 생성 진입 시 상세 해제를 완료 시점이 아니라 진입 시점에 둔 이유

Summary by CodeRabbit

  • 새 기능
    • 시크릿 상세 화면에서 유형별 정보, 연결 프로젝트, 만료일 및 메타데이터를 읽기 전용으로 확인할 수 있습니다.
    • 민감한 값의 마스킹, 복사, 즐겨찾기, 삭제 및 복호화 재시도를 지원합니다.
    • 다중 프로젝트 칩, 여러 줄 텍스트, 읽기 전용 라디오 버튼 및 아이콘 필드가 추가되었습니다.
    • 화면 크기에 따라 폼과 컴포넌트가 더욱 유연하게 배치됩니다.
  • 개선
    • 만료 임박 및 만료 시 위험·경고 아이콘과 색상으로 표시합니다.
    • 시크릿 생성·수정·삭제 후 목록과 선택 상태가 안정적으로 갱신됩니다.

- DVComponentWidthPolicy(.fixed/.fill) Environment 값과 dvComponentWidth modifier 추가
- .fill에서 DVComponentSize를 최소 폭으로 해석해 컴포넌트가 컨테이너를 채우도록 함
- 기본값이 .fixed이므로 기존 호출부 동작은 변하지 않음
- 고정 .frame(width: size.width)를 dvComponentWidth modifier로 교체
- 대상: DVLabeledField / DVTextField / DVTextContainer / DVMultilineTextField / DVDropdown / DVChipsField / DVMultiSelectDropdown
- DVMultiSelectDropdown은 트리거만 적용하고 팝오버 패널 폭은 토큰 고정 유지
- isInteractive 파라미터(기본 true)와 init(readOnly:isSelected:) 편의 생성자 추가
- 읽기 전용에서는 탭·포커스·Space 핸들러를 뷰 트리에 부착하지 않고 .isButton 트레이트도 제외
- 다중 선택 값을 chip 그리드로 표시만 하는 컴포넌트 (DVMultiSelectDropdown 트리거의 read-only 대응)
- DVChip이 Button이라 컨테이너 전체에 allowsHitTesting(false)로 인터랙션 차단
- 값이 비어도 다른 읽기 전용 필드와 높이를 맞추도록 minHeight 28 유지
- FormLayoutMode를 FormLayout(해석 결과) + FormLayoutContext(화면 맥락)로 대체
- StandaloneFormLayout(CreateSecret) / DetailColumnFormLayout(detail 컬럼)이 각자 임계값과 사이즈 매핑을 소유
- 배열 프리셋 3개: dual / single / detailFluid — detailFluid는 컨테이너를 채우고 페어를 절반씩 나눔
- FormLayoutMetrics로 임계값(816)과 프레임 상한을 상수에서 계산하도록 통일
- AdaptiveFieldRow를 Sources/Form으로 이동하고 pairAxis 기반으로 전환
- 레이아웃과 무관한 isProjectLoading은 CreateSecretEnvironment로 분리
- Field 8종·Section 9종·CreateSecretView·PreviewWidth를 formLayoutMode에서 formLayout으로 전환
- 슬롯 사이즈 해석을 layout.size(for:) 한 줄로 통일
- ExpireDateFieldView가 내부 컨트롤을 .frame(width:)로 직접 고정하던 것을 dvComponentWidth로 교체 — fill 정책에서 래퍼만 늘어나 어긋나던 문제
- PreviewWidth 문서의 dual 임계값 표기를 실제 값(816)으로 정정
- 창을 넓히면 본문 필드는 제자리인데 footer 버튼만 오른쪽 끝까지 밀려나던 문제
- formMaxWidth()로 화면 루트 프레임을 816(2열 폼 콘텐츠 776 + 좌우 padding)에서 멈춤
- header/footer는 본문과 분리된 채 각자 이 프레임을 따라가므로 좌우 끝이 함께 움직임
- 컬럼이 창을 채우지 못하면 윈도우 배경이 양옆에 드러남
- 생성 화면 LabeledTextFieldView의 read-only 대응 (DVLabeledField + DVTextContainer)
- isSensitive / isCopyable 두 플래그의 4조합 분기 — 마스킹+복사 동시 조합만 accessories init 사용
- 마스킹·복사를 재구현하지 않고 DVTextContainer 편의 init에 위임
- 클립보드 쓰기(NSPasteboard)를 컴포넌트가 수행해 호출부는 isCopyable만 지정
- Figma 1768:30819 — 즐겨찾기(좌) + 타입명 + 공유/수정/삭제(우) + 서브타입 탭바
- 서브타입 탭바는 조회·수정 양쪽 모두 읽기 전용 — 서브타입이 바뀌면 payload 스키마가 달라져 수정이 아니라 재생성이 됨
- 탭바 간격은 24 — Figma 시각 간격 28에서 DVRadioButton 좌우 padding 4를 뺀 값
- 공유는 동작이 정해지지 않아 비활성으로 렌더
- displayName 조회를 위해 SecretType/SecretSubType → Creatable 역변환 추가 (6:6, 9:9 완전 대응)
- DetailSectionScaffoldView — 생성 화면 FormSectionScaffold의 read-only 대응
- 필드 순서를 CreateSecret SectionView와 동일하게 맞추고, 공통 페어 행 앞뒤로 payload 필드 슬롯 2개를 둠
- 공통 메타 필드는 Secret에서 바로 나오므로 payload 복호화를 기다리지 않고 그림
- Project는 다중 값이라 DetailProjectFieldView로 chip 표시 (생성 화면 DVMultiSelectDropdown 트리거와 동일한 모습)
- environment/expireDate 등 도메인 값의 표시 문자열 변환을 SecretDetailDisplayText로 분리
- 즐겨찾기 갱신은 PatchSecretUseCase.updateSimple 경유 — payload 복호화가 없어 생체인증을 타지 않음
- PatchSecretUseCase를 앱 타겟에 조립
- Secret 엔티티에 프로젝트 정보가 없어 연결 프로젝트는 FetchSecretUseCase.fetchProjects로 별도 조회
- 즐겨찾기 토글은 setLiked 성공 시 state.secret을 교체하고 secretUpdated delegate 발송
- 연타 시 응답 순서가 뒤바뀌어 liked가 반대로 굳는 것을 cancelInFlight로 방지
- 삭제는 확인 alert를 거친 뒤 softDelete 호출, 성공하면 deleted delegate 발송
- 진행 중 중복 조작을 막기 위해 isDeleting 추가
- state.secret을 let에서 internal(set) var로 변경 — reducer가 갱신해야 하고 private(set)은 State 선언 스코프 밖에서 쓸 수 없음
- 즐겨찾기·삭제 실패 alert 프리셋 추가
- Secret 엔티티에 프로젝트 정보가 없어 task에서 fetchLinkedProjects를 함께 조회
- 조회 실패 시 alert를 띄우지 않는다 — 부가 정보이고 payload 복호화 실패 alert와 겹치면 원인을 오해하게 됨
- 수정 모드 진입 시 projectIds 초기값으로도 재사용 예정
- close(×) 버튼과 Divider를 담고 있던 topBar 제거 — 닫기는 사이드바 전환·리스트 선택 해제로만
- DetailColumnFormLayout으로 레이아웃을 측정해 주입하고 formMaxWidth로 프레임 상한 적용
- 헤더와 공통 메타 섹션 연결. 수정 버튼은 상태 전이가 없어 비활성으로 렌더
- 삭제 진행 중에는 헤더 전체를 disabled 처리
- task(id:)로 교체 — 다른 리스트 셀을 선택하면 새 State가 할당되지만 뷰 아이덴티티가 그대로여서 .task가 재실행되지 않아 연결 프로젝트가 빈 채로 남던 문제
- finish()를 await해 secret 전환 시 이전 조회 effect가 취소되도록 함
- secretUpdated / deleted delegate 수신 시 목록을 재조회
- 항목 교체가 아니라 재조회인 이유: liked·expired·project 같은 필터 컬렉션에서 조건을 벗어난 항목이 그대로 남음 (Star 컬렉션에서 즐겨찾기를 해제해도 행이 보이는 문제)
- deleted 수신 시 secretDetail과 selectedSecretID 초기화
- SecretListFeature에 refresh 액션 추가 — task/didTapRetry와 달리 loading으로 바꾸지 않아 목록이 깜빡이지 않음
- 즐겨찾기 3종: 성공 시 secret 교체 + delegate, 현재 값의 반대값 전달, 실패 시 alert
- 삭제 3종: 확인 alert만 띄우고 즉시 삭제하지 않음, 성공 시 deleted delegate, 실패 시 isDeleting 해제
- 연결 프로젝트 2종: 로드 성공, 실패 시 alert 없음
- MainFeature 2종: secretUpdated·deleted 수신 시 목록 재조회 및 detail 유지/해제
- task 테스트의 merge 수신 순서 의존 제거 — 관심 없는 dependency를 CancellationError로 스텁하면 액션이 발행되지 않아 순서와 무관해짐
- 인증 취소 시 alert를 닫으면 다시 시도할 방법이 없어 화면에 갇히던 문제
- didTapRetryReveal은 reveal effect만 재실행한다 — projects·연결 프로젝트는 재조회하지 않음
- .task와 재시도가 공유하도록 revealEffect(secret:)를 헬퍼로 추출
- cancelInFlight로 인증 프롬프트가 중복 누적되지 않게 처리
- 실패 원인별 안내 문구를 revealFailureMessage로 분리 — alert는 닫히면 사라지므로 화면에서도 원인이 읽혀야 한다
- 재시도 성공·실패와 인증 취소 후 재시도 흐름 테스트 추가
- 인증 전에는 필드 스캐폴드를 뷰 트리에서 제외하고 남은 영역을 로딩·실패 뷰가 채운다
- 반투명 오버레이로 덮는 방식을 버린 이유: 뒤의 필드가 비쳐 보여 어디까지가 복호화된 정보인지 구분되지 않는다
- 헤더는 두 경우 모두 남긴다 — 즐겨찾기·삭제는 복호화 없이 수행되므로 로딩 중에도 열려 있어야 한다
- 복호화 전 경로는 ScrollView로 감싸지 않는다. 감싸면 상태 뷰가 남은 높이를 채우지 못해 헤더 바로 아래에 작게 붙는다
- 헤더와 본문의 padding·정렬이 두 경로에서 어긋나지 않도록 bodyStack으로 묶음
- 실패 시 안내 문구와 재시도 버튼 노출 (SecretListView 에러 표현과 스타일 일치)
- 기존에는 secretType만 분기해 accessToken·webhookSecret·serviceAccount·sslTlsCertificate·custom이 실제와 다른 payload 타입을 반환했다
- live dispatchRevealPayload와 case 구성·순서·default 처리를 1:1로 일치시킴
- metadata도 채워 조회 화면에서 metadata 기반 필드까지 프리뷰로 확인 가능하게 함
- CreateSecret 폼이 입력받지 않는 metadata 필드는 live 경로에서도 항상 nil이라 그대로 nil 유지
- 한쪽만 바뀌면 프리뷰가 조용히 어긋나므로 정합 유지 제약을 주석으로 남김
- 기존 픽스처 14개가 전부 subType nil이어서 dummy revealPayload의 서브타입 분기에 도달할 수 없었다
- previewSubTypeMatrix 신설 — dummy·live가 분기하는 11개 (secretType, subType) 조합 전수 커버
- secret 헬퍼에 subType·environment·memo 파라미터 추가 (전부 기본값 nil이라 기존 호출부 무변경)
- 메타 필드는 값 있는 항목과 nil 항목을 섞음 — optional 필드의 빈 상태 렌더도 프리뷰로 확인해야 한다
- preview 배열은 아바타 폴백·컬렉션 필터 검증 의도로 구성이 맞춰져 있어 수정하지 않음
- 공통 페어 행(Project/Services/Expire Date/Environment)을 스캐폴드에서 제거하고 각 섹션이 조립하도록 변경
- 구성이 타입마다 달라서다 — SSHKey·SSLTLSCert·EnvSet은 Project|Environment에 둘째 행이 없고, ServiceAccount는 Expire Date가 단독 행, LicenseKey는 Environment 자리에 License Tier가 온다
- 고정 구성으로는 9개 중 4개만 맞았다. 생성 화면 FormSectionScaffold도 같은 이유로 Name·Memo만 갖는다
- primary/trailing 2슬롯을 단일 content 슬롯으로 통합하고 linkedProjects는 섹션이 받도록 이동
- 각 섹션이 DetailSectionScaffoldView를 감싸고 자기 공통 행을 조립 — 생성 화면 SectionView와 1:1 미러링
- 라벨·필드 순서·sizeMode를 대응 생성 섹션에서 그대로 가져옴
- SSL Required와 License Tier는 조회에서 값 변경 컨트롤을 쓸 수 없어 텍스트로 표시. metadata가 nil이면 Not Required로 해석
- isSensitive·isCopyable은 필드별로 지정 — 실제 비밀값만 마스킹, 복사해 쓰는 값은 복사만, 짧은 메타는 표시만
- 디스패처는 default 없이 11 case를 모두 명시해 도메인에 case가 늘면 컴파일러가 잡도록 함
- 프리뷰는 Store·dependency 없이 payload 값만 주입. 조합 전수는 sweep 프리뷰 하나로 훑는다
- payloadState가 .loaded일 때 DetailPayloadSectionView를 렌더
- 복호화 전 경로(ScrollView 미사용·bodyStack·헤더 유지)는 그대로 유지
- 소비처가 사라진 isPayloadRevealed 판정 제거
- 프리뷰 픽스처를 previewSubTypeMatrix 참조로 교체 — 파일마다 secret을 새로 만들지 않는다
- task에서 payloadState가 .loaded면 reveal effect를 태우지 않는다
- 평문이 State에 남아 있어 재인증이 보안을 더해주지 않고, 뷰가 다시 나타날 때마다 Touch ID가 뜨는 것만 성가시다
- .failed는 가드하지 않는다 — 재진입은 재시도 기회다
- 프로젝트·연결 프로젝트 조회는 가드와 무관하게 그대로 수행
- revealPayload 호출 횟수로 검증하는 테스트 추가
- 생성 화면이 그 타입에서 입력받지 않는 필드를 픽스처가 채우고 있었다 — 실제 데이터에 존재할 수 없는 상태라 조회 화면에서 값이 왜 안 보이는지 오해하게 된다
- serviceAccount의 environment 제거 (Environment 입력 없음)
- sshKey·sslTlsCertificate의 expiresAt 제거 (Expire Date 입력 없음)
- 만료 케이스는 Expire Date를 실제로 입력받는 webhookSecret으로 이동
- 타입별 입력 가능 필드 표를 배열 doc에 남겨 같은 실수를 막는다
- 스캐폴드가 formLayout을 Environment로 읽게 되면서 본문에서 layout 참조가 사라졌다
- 파라미터로 받는 이유를 설명하던 doc도 함께 정리
- EnvSet placeholder가 화면에 리터럴 백슬래시+n을 그대로 노출하고 있었다. 한 줄 필드이므로 개행 예시를 뺀다
- projectIds 주석이 UI가 단일 선택으로 제한한다고 적혀 있으나 DVMultiSelectDropdown에 개수 제한이 없어 실제로는 다중 선택된다. 다중 연결이 의도된 동작이므로 주석을 실제에 맞춘다
- DVTextContainer가 lineLimit(1)이라 개행이 있는 값은 첫 줄만 보이고 나머지를 확인할 방법이 없었다.
- DVMultilineTextField의 read-only 대응으로 DVMultilineTextContainer 추가. 스타일은 DVTextContainer와 동일하게 맞추고 내용이 넘치면 세로 스크롤
- 마스킹은 줄 단위로 채워 한 줄 값일 때 기존 출력과 동일하고, 여러 줄에서는 줄 구조가 남아 필드가 제대로 채워졌는지 확인할 수 있다
- 개행 판정은 DetailReadOnlyFieldView 한 곳에서 하므로 섹션 호출부는 변경 없음
- 빈 값에는 복사 버튼을 달지 않는다
- Figma 1529:139917 / 1529:139939 반영. 3일 이내는 danger + 경고 삼각형, 7일 이내는 warning + calendar
- 임계값을 SecretExpiryStatus 한 곳에 모았다. 목록과 조회가 각자 들고 있으면 같은 시크릿이 한쪽에서만 강조되는 모순이 생긴다
- 판정은 SecretExpiryStatus, 표현은 DVExpiryEmphasis로 나눴다.
- 날짜를 yy.MM.dd 고정으로 바꿨다. 추후, 정책 확정 시 처리(locale)
- addButtonTapped에서 secretDetail과 secretList.selectedSecretID를 정리
- 완료·취소 어느 경로로 나가도 stale 상태가 남지 않도록 secretCreated가 아니라 진입 시점에서 끊음
- generate-local 워크스페이스는 ad-hoc 서명이라 Touch ID 평가가 실패한다. 온보딩의 Touch ID 단계와 잠금 해제가 모두 인증을 요구해 메인 화면까지 도달할 수 없으므로 우회 통로를 만든다
- AppFeature에 DEBUG 전용 debugSkipToMain을 추가해 onboarding·locked를 건너뛰고 main으로 진입
- hasCompletedOnboarding은 세우지 않는다. 다음 실행에서 온보딩이 다시 나와야 실제 흐름을 계속 검증할 수 있고 디버그 통로가 영구 상태로 굳지 않는다
- Debug 메뉴에서 옵션+커맨드+M으로 실행 (CommandMenu, 파일 전체 #if DEBUG)
- 메뉴가 같은 Store를 참조해야 해서 DevaultApp의 Store를 프로퍼티로 승격. ViewBuilder 안에서 생성되어 body 재평가 시 Store가 새로 만들어질 수 있던 문제도 함께 해소
- 팀 시트 발급으로 generate-local이 필요 없어지면 제거하도록 TODO에 #64 연결
@doyeonk429 doyeonk429 self-assigned this Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f4e827ff-5be2-45aa-9665-4b6770f4d3a6

📥 Commits

Reviewing files that changed from the base of the PR and between 3bcdc2f and e728659.

📒 Files selected for processing (8)
  • Projects/DVDesign/Sources/Components/DVMultilineTextContainer.swift
  • Projects/DVPresentation/Sources/Features/Main/MainFeature.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailReadOnlyFieldView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailDisplayText.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailError.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailView.swift
  • Projects/DVPresentation/Sources/Form/FormLayout.swift
  • Projects/DVPresentation/Tests/Main/MainFeatureTests.swift
🚧 Files skipped from review as they are similar to previous changes (7)
  • Projects/DVPresentation/Sources/Features/Main/MainFeature.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailDisplayText.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailError.swift
  • Projects/DVPresentation/Sources/Form/FormLayout.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailReadOnlyFieldView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailView.swift
  • Projects/DVPresentation/Tests/Main/MainFeatureTests.swift

Walkthrough

시크릿 상세 조회 화면을 9개 payload 유형의 읽기 전용 섹션으로 확장했습니다. 공통 폼 레이아웃과 폭 정책을 도입하고, 만료 강조·연결 프로젝트·즐겨찾기·삭제·복호화 재시도 흐름을 연결했습니다. DEBUG 메인 진입 명령과 미리보기 데이터도 추가했습니다.

Changes

시크릿 상세 조회

Layer / File(s) Summary
읽기 전용 컴포넌트와 payload 섹션
Projects/DVDesign/Sources/Components/*, Projects/DVPresentation/Sources/Features/SecretDetail/Components/*
텍스트, 여러 줄 텍스트, 칩, 라디오 버튼과 9개 payload별 상세 섹션을 추가했습니다. 민감 값 마스킹과 복사를 지원합니다.
상세 상태와 액션
Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailFeature.swift, SecretDetailView.swift
payload 복호화, 연결 프로젝트 조회, 재시도, 즐겨찾기 변경과 삭제 확인을 처리합니다.

폼 레이아웃과 만료 표현

Layer / File(s) Summary
공통 폼 레이아웃
Projects/DVPresentation/Sources/Form/*, Projects/DVPresentation/Sources/Features/CreateSecret/*
FormLayout과 레이아웃 컨텍스트를 추가했습니다. 기존 formLayoutMode 사용을 formLayout(_:)으로 전환했습니다.
컴포넌트 폭 정책
Projects/DVDesign/Sources/Foundations/Sizing/*, Projects/DVDesign/Sources/Components/*
dvComponentWidth modifier를 추가하고 고정 frame 사용을 정책 기반 폭 적용으로 변경했습니다.
만료 상태 통합
Projects/DVPresentation/Sources/Support/SecretExpiryStatus.swift, Projects/DVDesign/Sources/Foundations/Expiry/*, Projects/DVPresentation/Sources/Features/SecretList/*
3일과 7일 기준으로 만료 상태를 계산하고 DVExpiryEmphasis로 표현합니다.

데이터와 앱 연결

Layer / File(s) Summary
SecretClient 동작
Projects/DVPresentation/Sources/Dependencies/*, Projects/Devault/Sources/Composition/Dependencies/*
payload별 preview 데이터를 추가하고 setLiked, fetchLinkedProjects를 preview/live 의존성에 연결했습니다.
목록 갱신과 앱 상태
Projects/DVPresentation/Sources/Features/Main/*, Projects/DVPresentation/Sources/Features/SecretList/*, Projects/Devault/Sources/DevaultApp.swift
수정·삭제 후 목록과 사이드바 개수를 갱신합니다. 삭제와 새 시크릿 생성 시 상세 상태와 선택 상태를 초기화합니다.
DEBUG 진입 명령
Projects/DVPresentation/Sources/Features/AppFeature.swift, Projects/Devault/Sources/DevaultDebugCommands.swift
DEBUG 메뉴와 ⌘⌥M 단축키로 온보딩과 잠금을 건너뜁니다.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: ✨ Feature, 🎨 Design

Suggested reviewers: yeseonglee

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning [#75] 범위를 넘어 폭 정책, 생성 폼 개편, 만료 정책, 디버그 명령, 즐겨찾기·삭제 흐름까지 포함합니다. 조회 화면 구현과 직접 관련된 변경만 유지하고, 폭 정책·생성 폼·만료 정책·디버그 명령·즐겨찾기·삭제 변경은 별도 PR로 분리하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% 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 제목이 9개 타입의 시크릿 조회 화면 구현과 가변 폼 레이아웃 도입이라는 주요 변경을 정확히 요약합니다.
Linked Issues check ✅ Passed [#75] 읽기 전용 필드, 비인터랙티브 헤더, 타입별 섹션, SecretDetailView 구현 요구사항을 모두 반영했습니다.
✨ 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/#75

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

🧹 Nitpick comments (10)
Projects/DVDesign/Sources/Components/DVMultilineTextContainer.swift (1)

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

fileprivateprivate로 축소하세요.

MultilineContainerMetrics는 Line 91에서만 참조됩니다. 파일 내부 전체에 노출할 필요가 없습니다. private로 변경해 접근 범위를 축소하세요.

수정 예시
-fileprivate enum MultilineContainerMetrics {
+private enum MultilineContainerMetrics {

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/DVDesign/Sources/Components/DVMultilineTextContainer.swift` at line
77, Change the access modifier on MultilineContainerMetrics from fileprivate to
private, since it is only referenced within its enclosing scope at line 91;
leave its definition and usage otherwise unchanged.

Sources: Path instructions, Linters/SAST tools

Projects/DVDesign/Sources/Components/DVChipsContainer.swift (1)

60-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

SwiftUI 하위 레이아웃을 private extension으로 분리하세요.

body는 화면 조합만 담당하게 유지하세요. 하위 레이아웃과 하위뷰를 private var 또는 private func로 이동하세요.

  • Projects/DVDesign/Sources/Components/DVChipsContainer.swift#L60-L75: DVFlowLayout 구성을 private var chipsContent로 분리하세요.
  • Projects/DVDesign/Sources/Components/DVMultilineTextContainer.swift#L135-L148: 컨테이너 HStack 구성을 private var content로 분리하세요.
  • Projects/DVDesign/Sources/Components/DVTextContainer.swift#L146-L183: 행 레이아웃과 leadingIconViewprivate extension DVTextContainer으로 이동하세요.
  • Projects/DVDesign/Sources/Components/RadioButton/DVRadioButton.swift#L108-L147: labelRowindicatorprivate extension DVRadioButton으로 이동하세요.

As per path instructions, var body 안에 중첩 레이아웃이 직접 구현되어 있으면 extension의 private var/func로 분리를 제안하세요.하위뷰가 extension 밖에 선언되어 있으면 이동을 제안하세요.

🤖 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/DVChipsContainer.swift` around lines 60
- 75, Refactor the SwiftUI sublayouts out of body into private extensions or
private computed properties: in
Projects/DVDesign/Sources/Components/DVChipsContainer.swift:60-75, move the
DVFlowLayout into private var chipsContent; in
Projects/DVDesign/Sources/Components/DVMultilineTextContainer.swift:135-148,
move the HStack into private var content; in
Projects/DVDesign/Sources/Components/DVTextContainer.swift:146-183, move the row
layout and leadingIconView into private extension DVTextContainer; and in
Projects/DVDesign/Sources/Components/RadioButton/DVRadioButton.swift:108-147,
move labelRow and indicator into private extension DVRadioButton. Keep each body
focused on composition and move any related subviews inside the appropriate
private extension.

Source: Path instructions

Projects/DVPresentation/Sources/Form/AdaptiveFieldRow.swift (1)

47-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

body에서 행 레이아웃을 분리하세요.

Line 47과 Line 63은 body에서 HStackVStack를 직접 구현합니다. horizontalRow(spacing:)verticalRow(spacing:)private extension AdaptiveFieldRowprivate func로 분리하세요. body에는 pairAxis 분기만 유지하세요.

As per path instructions, "var body 안에 중첩 레이아웃이 직접 구현되어 있으면 extensionprivate var/func로 분리를 제안하세요."

🤖 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/Form/AdaptiveFieldRow.swift` around lines 47
- 72, AdaptiveFieldRow의 body에서 직접 구현된 HStack와 VStack 레이아웃을 분리하세요. body에는
layout.pairAxis에 따른 분기와 horizontalRow(spacing:), verticalRow(spacing:) 호출만 남기고,
각 레이아웃 구현은 private extension AdaptiveFieldRow의 private func
horizontalRow(spacing:) 및 private func verticalRow(spacing:)로 이동하세요.

Source: Path instructions

Projects/DVPresentation/Sources/Features/CreateSecret/CreateSecretView.swift (1)

24-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

body의 중첩 레이아웃을 private 구성요소로 분리하세요.

GeometryReader 내부의 VStack과 폭 계산이 body에 직접 남아 있습니다. 이번 변경으로 패딩, 최대 폭, FormLayout 환경 주입이 같은 체인에 추가되었습니다. extension CreateSecretViewprivate func formContent(width: CGFloat) -> some View를 두고 body에서는 호출만 하세요.

As per path instructions: var body 안에 중첩 레이아웃이 직접 구현되어 있으면 extensionprivate var/func로 분리를 제안하세요.

🤖 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/CreateSecret/CreateSecretView.swift`
around lines 24 - 28, Extract the nested GeometryReader/VStack form layout from
CreateSecretView.body into a private formContent(width: CGFloat) -> some View
function in an extension CreateSecretView. Keep the existing padding,
formMaxWidth(), and formLayout(StandaloneFormLayout.layout(for:
proxy.size.width)) modifiers unchanged, and have body only invoke the extracted
component.

Source: Path instructions

Projects/DVPresentation/Sources/Features/SecretDetail/Components/SecretDetailHeaderView.swift (1)

48-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

body의 레이아웃을 extension의 private 서브뷰로 분리하세요.

bodyVStack과 탭바 조건 분기를 직접 소유합니다. 이 구성을 extension의 private var content로 이동하고 body에서는 해당 서브뷰만 반환하세요.

As per path instructions, "var body 안에 중첩 레이아웃이 직접 구현되어 있으면 extension의 private var/func로 분리를 제안하세요."

🤖 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/SecretDetail/Components/SecretDetailHeaderView.swift`
around lines 48 - 55, SecretDetailHeaderView의 body에 직접 구현된 VStack 레이아웃과 tabBar
조건 분기를 extension의 private var content로 이동하세요. body는 해당 content 서브뷰만 반환하도록 변경하고,
spacing·typeRow·tabBar 및 frame 동작은 그대로 유지하세요.

Source: Path instructions

Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailSectionScaffoldView.swift (1)

22-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

프로퍼티 접근 수준을 통일하세요.

contentprivate이고 secret은 internal입니다. secret도 뷰 내부에서만 읽으므로 private으로 좁힐 수 있습니다. 다른 Detail 섹션 뷰들과 함께 정책을 맞추면 의도가 분명해집니다.

♻️ 제안 diff
-    let secret: Secret
+    private let secret: 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/SecretDetail/Components/DetailSectionScaffoldView.swift`
around lines 22 - 24, DetailSectionScaffoldView의 secret 프로퍼티 접근 수준을 private으로
변경하세요. content의 기존 private 접근 수준과 다른 Detail 섹션 뷰들의 정책을 유지해 뷰 내부에서만 읽을 수 있도록
맞추세요.

Source: Path instructions

Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailView.swift (1)

106-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

수직 여백에 수평 상수를 재사용하지 마세요.

L111이 .padding(.vertical, FormLayoutMetrics.horizontalPadding)을 사용합니다. 값은 같아도 이름과 용도가 어긋나 이후 수평 여백만 바꾸면 수직 여백까지 함께 바뀝니다. FormLayoutMetrics에 수직 상수를 추가하고 그 값을 쓰세요.

♻️ 제안 diff
         .padding(.horizontal, FormLayoutMetrics.horizontalPadding)
-        .padding(.vertical, FormLayoutMetrics.horizontalPadding)
+        .padding(.vertical, FormLayoutMetrics.verticalPadding)

FormLayout.swift에 상수를 추가하세요.

enum FormLayoutMetrics {
    static let verticalPadding: CGFloat = horizontalPadding
}
🤖 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/SecretDetail/SecretDetailView.swift`
around lines 106 - 113, Update the VStack padding in SecretDetailView to use a
dedicated FormLayoutMetrics.verticalPadding constant instead of
horizontalPadding, and add that verticalPadding constant in FormLayout.swift
while preserving the current value.
Projects/DVPresentation/Sources/Dependencies/SecretClient.swift (1)

145-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

default 대신 남은 조합을 명시하세요.

주석은 live dispatchRevealPayload와 case 구성이 1:1로 일치해야 한다고 말합니다. 그런데 default가 그 안전망을 없앱니다. SecretType이나 SecretSubType에 case를 추가해도 컴파일러가 이 switch를 지적하지 않습니다. 남은 조합을 명시적으로 나열하고, 유효하지 않은 조합만 한 곳에서 처리하세요.

♻️ 제안 방향
-          default:
+          case (.apiKeyToken, _), (.oauth, _), (.sshAndCredentials, _), (.etc, _):
               assertionFailure("Unexpected (secretType, subType) combination: \(secret.secretType), \(String(describing: secret.subType))")
               throw SecretUseCaseError.unexpected
🤖 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/Dependencies/SecretClient.swift` around lines
145 - 148, Update the switch in the secret dispatch logic around the
`(secret.secretType, secret.subType)` handling to remove the default branch and
explicitly enumerate every remaining valid combination from live
dispatchRevealPayload. Keep the assertion and SecretUseCaseError.unexpected
handling in a single branch for only invalid combinations, while preserving
exhaustive compiler checking when SecretType or SecretSubType gains a new case.
Projects/DVPresentation/Sources/Dependencies/SecretClient+Preview.swift (1)

79-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

프리뷰가 previewSubTypeMatrix를 위치 인덱스로 참조합니다. 픽스처 배열의 순서가 곧 계약이 되어 있습니다. 항목을 하나 추가하거나 재정렬하면 프리뷰가 조용히 다른 타입을 그리고, 항목을 줄이면 인덱스 범위를 벗어나 크래시합니다. 배열 정의 쪽에 (secretType, subType)으로 조회하는 접근자 또는 명명된 정적 프로퍼티를 추가하고, 각 프리뷰가 그것을 쓰게 하세요.

  • Projects/DVPresentation/Sources/Dependencies/SecretClient+Preview.swift#L79-L167: previewSubTypeMatrixstatic func previewSecret(_ type: SecretType, _ subType: SecretSubType?) -> Secret 같은 조회 헬퍼 또는 previewAPIKey, previewCustom 등 명명된 프로퍼티를 추가하세요.
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailSectionScaffoldView.swift#L58-L100: [0], [1]을 명명된 픽스처로 교체하세요.
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailAPIKeysTokenSectionView.swift#L60-L88: [0], [1]을 apiKey / accessToken 픽스처로 교체하세요.
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailDatabaseSectionView.swift#L63-L90: [5]를 database 픽스처로 교체하세요.
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailEnvSetSectionView.swift#L46-L70: [8]을 envSet 픽스처로 교체하세요.
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailServiceAccountSectionView.swift#L57-L85: [4]를 serviceAccount 픽스처로 교체하세요.
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailCustomSectionView.swift#L54-L78: [10]을 custom 픽스처로 교체하세요.
🤖 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/Dependencies/SecretClient`+Preview.swift
around lines 79 - 167, Replace positional previewSubTypeMatrix access with
stable named lookup fixtures. In
Projects/DVPresentation/Sources/Dependencies/SecretClient+Preview.swift#L79-L167,
add a (SecretType, SecretSubType?) lookup helper or named static properties;
update DetailSectionScaffoldView.swift#L58-L100 to use the corresponding named
fixtures, DetailAPIKeysTokenSectionView.swift#L60-L88 to use apiKey/accessToken,
DetailDatabaseSectionView.swift#L63-L90 to use database,
DetailEnvSetSectionView.swift#L46-L70 to use environmentVariableSet,
DetailServiceAccountSectionView.swift#L57-L85 to use serviceAccount, and
DetailCustomSectionView.swift#L54-L78 to use custom.
Projects/DVPresentation/Tests/Main/MainFeatureTests.swift (1)

112-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

테스트 Secret 생성을 헬퍼로 묶으세요.

이번 변경으로 동일한 Secret(...) 생성 블록이 3곳 더 늘었습니다. SecretDetailFeatureTestsmakeSecret처럼 정적 헬퍼를 하나 두고 이름·liked만 인자로 받으세요. 이후 Secret 이니셜라이저가 바뀌면 수정 지점이 한 곳으로 줄어듭니다.

♻️ 제안 diff
+  private static func makeSecret(
+    name: String = "Test Token",
+    liked: Bool = false
+  ) -> Secret {
+    Secret(
+      id: UUID(),
+      name: name,
+      secretType: .apiKeyToken,
+      liked: liked,
+      createdAt: Date(),
+      updatedAt: Date(),
+      payload: SecretPayload(encryptedData: Data(), keyTag: "test", schemaVersion: 1)
+    )
+  }

Also applies to: 368-376, 400-407

🤖 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/Tests/Main/MainFeatureTests.swift` around lines 112 -
119, Extract the repeated Secret(...) construction in MainFeatureTests into a
static makeSecret helper, following SecretDetailFeatureTests.makeSecret. Have
the helper accept only name and liked, centralize the remaining default fields
and payload values there, and replace all three duplicated construction blocks
with calls to this helper.
🤖 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/DVDesign/Sources/Components/DVMultilineTextContainer.swift`:
- Around line 238-246: DVMultilineTextContainerPreviewData의 pem 샘플에서 PEM
private-key 헤더와 형식화된 키 데이터를 제거하세요. 여러 줄 스크롤 프리뷰가 계속 동작하도록 동일한 용도의 더미 멀티라인 문자열을
사용하되, 정적 분석기가 키로 인식할 수 있는 PEM 형식은 포함하지 마세요.

In `@Projects/DVPresentation/Sources/Dependencies/SecretClient.swift`:
- Around line 150-154: Update the setLiked stub to use the requested secret ID
instead of always starting from Secret.preview. Resolve and return the fixture
corresponding to the request ID, then apply the liked value while preserving
that secret’s existing fields.

In `@Projects/DVPresentation/Sources/Features/Main/MainFeature.swift`:
- Around line 107-115: Update the .secretDetail(.delegate(.secretUpdated)) and
.deleted cases in MainFeature to also send sidebar(.countsRefreshRequested),
alongside the existing secretList(.refresh) action, so sidebar collection counts
refresh after edits and deletions.

In
`@Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailReadOnlyFieldView.swift`:
- Around line 297-301: Remove the PEM private-key markers and key-shaped sample
from the preview content in DetailReadOnlyFieldView, replacing it with a
multiline placeholder that cannot be detected as a private key while preserving
the preview’s placeholder purpose.

In
`@Projects/DVPresentation/Sources/Features/SecretDetail/Components/SecretDetailHeaderView.swift`:
- Around line 29-30: Update SecretDetailHeaderView’s type inputs so its title
and selected subtype are derived from the decrypted CreateSecretPayload case
used by DetailPayloadSectionView, rather than only store.secret.secretType and
store.secret.subType. Pass the normalized type/subtype from the decrypted
payload into the header, or show an error state whenever the metadata and
payload cannot be reconciled.

In
`@Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailEnvSetSectionView.swift`:
- Around line 20-25: Update the label passed to DetailReadOnlyFieldView in
DetailEnvSetSectionView from “envSet List” to Title Case “EnvSet List”, matching
the capitalization used by labels such as “Link String” and “Certification
JSON”.

In
`@Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailDisplayText.swift`:
- Around line 35-40: Update the expiration-date creation mapping and
SecretDetailDisplayText.expireDateDisplayText formatter to use the same fixed
calendar and time zone, rather than Calendar.current and the device’s default
time zone. Ensure the 30-day initial expiration and formatted expiration date
both follow the stored local-date-at-midnight convention.

In
`@Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailError.swift`:
- Around line 38-44: Update SecretDetailError.revealFailureMessage so
.unexpected returns a separate secret-load failure message rather than the
decryption failure message, while keeping .decryptionFailed mapped to the
existing decryption text and leaving .authRequired unchanged.

---

Nitpick comments:
In `@Projects/DVDesign/Sources/Components/DVChipsContainer.swift`:
- Around line 60-75: Refactor the SwiftUI sublayouts out of body into private
extensions or private computed properties: in
Projects/DVDesign/Sources/Components/DVChipsContainer.swift:60-75, move the
DVFlowLayout into private var chipsContent; in
Projects/DVDesign/Sources/Components/DVMultilineTextContainer.swift:135-148,
move the HStack into private var content; in
Projects/DVDesign/Sources/Components/DVTextContainer.swift:146-183, move the row
layout and leadingIconView into private extension DVTextContainer; and in
Projects/DVDesign/Sources/Components/RadioButton/DVRadioButton.swift:108-147,
move labelRow and indicator into private extension DVRadioButton. Keep each body
focused on composition and move any related subviews inside the appropriate
private extension.

In `@Projects/DVDesign/Sources/Components/DVMultilineTextContainer.swift`:
- Line 77: Change the access modifier on MultilineContainerMetrics from
fileprivate to private, since it is only referenced within its enclosing scope
at line 91; leave its definition and usage otherwise unchanged.

In `@Projects/DVPresentation/Sources/Dependencies/SecretClient.swift`:
- Around line 145-148: Update the switch in the secret dispatch logic around the
`(secret.secretType, secret.subType)` handling to remove the default branch and
explicitly enumerate every remaining valid combination from live
dispatchRevealPayload. Keep the assertion and SecretUseCaseError.unexpected
handling in a single branch for only invalid combinations, while preserving
exhaustive compiler checking when SecretType or SecretSubType gains a new case.

In `@Projects/DVPresentation/Sources/Dependencies/SecretClient`+Preview.swift:
- Around line 79-167: Replace positional previewSubTypeMatrix access with stable
named lookup fixtures. In
Projects/DVPresentation/Sources/Dependencies/SecretClient+Preview.swift#L79-L167,
add a (SecretType, SecretSubType?) lookup helper or named static properties;
update DetailSectionScaffoldView.swift#L58-L100 to use the corresponding named
fixtures, DetailAPIKeysTokenSectionView.swift#L60-L88 to use apiKey/accessToken,
DetailDatabaseSectionView.swift#L63-L90 to use database,
DetailEnvSetSectionView.swift#L46-L70 to use environmentVariableSet,
DetailServiceAccountSectionView.swift#L57-L85 to use serviceAccount, and
DetailCustomSectionView.swift#L54-L78 to use custom.

In
`@Projects/DVPresentation/Sources/Features/CreateSecret/CreateSecretView.swift`:
- Around line 24-28: Extract the nested GeometryReader/VStack form layout from
CreateSecretView.body into a private formContent(width: CGFloat) -> some View
function in an extension CreateSecretView. Keep the existing padding,
formMaxWidth(), and formLayout(StandaloneFormLayout.layout(for:
proxy.size.width)) modifiers unchanged, and have body only invoke the extracted
component.

In
`@Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailSectionScaffoldView.swift`:
- Around line 22-24: DetailSectionScaffoldView의 secret 프로퍼티 접근 수준을 private으로
변경하세요. content의 기존 private 접근 수준과 다른 Detail 섹션 뷰들의 정책을 유지해 뷰 내부에서만 읽을 수 있도록
맞추세요.

In
`@Projects/DVPresentation/Sources/Features/SecretDetail/Components/SecretDetailHeaderView.swift`:
- Around line 48-55: SecretDetailHeaderView의 body에 직접 구현된 VStack 레이아웃과 tabBar 조건
분기를 extension의 private var content로 이동하세요. body는 해당 content 서브뷰만 반환하도록 변경하고,
spacing·typeRow·tabBar 및 frame 동작은 그대로 유지하세요.

In
`@Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailView.swift`:
- Around line 106-113: Update the VStack padding in SecretDetailView to use a
dedicated FormLayoutMetrics.verticalPadding constant instead of
horizontalPadding, and add that verticalPadding constant in FormLayout.swift
while preserving the current value.

In `@Projects/DVPresentation/Sources/Form/AdaptiveFieldRow.swift`:
- Around line 47-72: AdaptiveFieldRow의 body에서 직접 구현된 HStack와 VStack 레이아웃을 분리하세요.
body에는 layout.pairAxis에 따른 분기와 horizontalRow(spacing:), verticalRow(spacing:)
호출만 남기고, 각 레이아웃 구현은 private extension AdaptiveFieldRow의 private func
horizontalRow(spacing:) 및 private func verticalRow(spacing:)로 이동하세요.

In `@Projects/DVPresentation/Tests/Main/MainFeatureTests.swift`:
- Around line 112-119: Extract the repeated Secret(...) construction in
MainFeatureTests into a static makeSecret helper, following
SecretDetailFeatureTests.makeSecret. Have the helper accept only name and liked,
centralize the remaining default fields and payload values there, and replace
all three duplicated construction blocks with calls to this helper.
🪄 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: 35ba77b0-43c6-4915-97da-afea8582695e

📥 Commits

Reviewing files that changed from the base of the PR and between 74352f5 and 3bcdc2f.

📒 Files selected for processing (73)
  • Projects/DVDesign/SampleApp/Sources/DVVaultContainerPreviewView.swift
  • Projects/DVDesign/Sources/Components/DVChipsContainer.swift
  • Projects/DVDesign/Sources/Components/DVChipsField.swift
  • Projects/DVDesign/Sources/Components/DVDropdown.swift
  • Projects/DVDesign/Sources/Components/DVLabeledField.swift
  • Projects/DVDesign/Sources/Components/DVMultiSelectDropdown.swift
  • Projects/DVDesign/Sources/Components/DVMultilineTextContainer.swift
  • Projects/DVDesign/Sources/Components/DVMultilineTextField.swift
  • Projects/DVDesign/Sources/Components/DVTextContainer.swift
  • Projects/DVDesign/Sources/Components/DVTextField.swift
  • Projects/DVDesign/Sources/Components/DVVaultContainer.swift
  • Projects/DVDesign/Sources/Components/RadioButton/DVRadioButton.swift
  • Projects/DVDesign/Sources/Foundations/Expiry/DVExpiryEmphasis.swift
  • Projects/DVDesign/Sources/Foundations/Sizing/DVComponentWidthPolicy.swift
  • Projects/DVPresentation/Sources/Dependencies/SecretClient+Preview.swift
  • Projects/DVPresentation/Sources/Dependencies/SecretClient.swift
  • Projects/DVPresentation/Sources/Features/AppFeature.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/CreateSecretEnvironment.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Fields/EnvironmentFieldView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Fields/ExpireDateFieldView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Fields/LabeledTextFieldView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Fields/LicenseTierFieldView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Fields/MemoFieldView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Fields/NameFieldView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Fields/ProjectFieldView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Fields/ServicesFieldView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/FormLayoutMode.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Sections/APIKeysTokenSectionView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Sections/CustomSectionView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Sections/DatabaseSectionView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Sections/EnvSetSectionView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Sections/LicenseKeySectionView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Sections/OAuthClientSectionView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Sections/SSHKeySectionView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Sections/SSLTLSCertSectionView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/Sections/ServiceAccountSectionView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/CreateSecretView.swift
  • Projects/DVPresentation/Sources/Features/CreateSecret/Model/SecretMetaFields.swift
  • Projects/DVPresentation/Sources/Features/Main/MainFeature.swift
  • Projects/DVPresentation/Sources/Features/Main/MainView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailExpireDateFieldView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailProjectFieldView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailReadOnlyFieldView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/DetailSectionScaffoldView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/SecretDetailHeaderView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailAPIKeysTokenSectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailCustomSectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailDatabaseSectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailEnvSetSectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailLicenseKeySectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailOAuthClientSectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailPayloadSectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailSSHKeySectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailSSLTLSCertSectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Components/Sections/DetailServiceAccountSectionView.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailDisplayText.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretDetailError.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/Model/SecretTypeReverseMapping.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailFeature.swift
  • Projects/DVPresentation/Sources/Features/SecretDetail/SecretDetailView.swift
  • Projects/DVPresentation/Sources/Features/SecretList/SecretListFeature.swift
  • Projects/DVPresentation/Sources/Features/SecretList/SecretListView.swift
  • Projects/DVPresentation/Sources/Form/AdaptiveFieldRow.swift
  • Projects/DVPresentation/Sources/Form/FormLayout.swift
  • Projects/DVPresentation/Sources/Form/FormLayoutContext.swift
  • Projects/DVPresentation/Sources/PreviewSupport/PreviewWidth.swift
  • Projects/DVPresentation/Sources/Support/SecretExpiryStatus.swift
  • Projects/DVPresentation/Tests/Features/SecretExpiryStatusTests.swift
  • Projects/DVPresentation/Tests/Main/MainFeatureTests.swift
  • Projects/DVPresentation/Tests/SecretDetail/SecretDetailFeatureTests.swift
  • Projects/Devault/Sources/Composition/Dependencies/SecretClient+Live.swift
  • Projects/Devault/Sources/DevaultApp.swift
  • Projects/Devault/Sources/DevaultDebugCommands.swift
💤 Files with no reviewable changes (1)
  • Projects/DVPresentation/Sources/Features/CreateSecret/Components/FormLayoutMode.swift

Comment thread Projects/DVDesign/Sources/Components/DVMultilineTextContainer.swift
Comment on lines +150 to +154
setLiked: { _, liked in
var secret = Secret.preview
secret.liked = liked
return secret
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

setLiked가 요청 id를 무시합니다.

이 스텁은 항상 Secret.preview("GitHub API Key")를 반환합니다. SecretDetailFeaturelikeResponse 성공 시 state.secret을 응답으로 교체합니다(Projects/DVPresentation/Tests/SecretDetail/SecretDetailFeatureTests.swifttoggleLike_success 참고). 그래서 프리뷰에서 다른 시크릿의 즐겨찾기를 누르면 상세 화면이 엉뚱한 시크릿으로 바뀝니다. 요청 id에 해당하는 픽스처를 찾아 반환하세요.

🐛 제안 diff
       setLiked: { id, liked in
-          var secret = Secret.preview
+          var secret = ([Secret].preview + [Secret].previewSubTypeMatrix)
+              .first { $0.id == id } ?? .preview
           secret.liked = liked
           return secret
       },
📝 Committable suggestion

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

Suggested change
setLiked: { _, liked in
var secret = Secret.preview
secret.liked = liked
return secret
},
setLiked: { id, liked in
var secret = ([Secret].preview + [Secret].previewSubTypeMatrix)
.first { $0.id == id } ?? .preview
secret.liked = liked
return 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/Dependencies/SecretClient.swift` around lines
150 - 154, Update the setLiked stub to use the requested secret ID instead of
always starting from Secret.preview. Resolve and return the fixture
corresponding to the request ID, then apply the liked value while preserving
that secret’s existing fields.

Comment thread Projects/DVPresentation/Sources/Features/Main/MainFeature.swift Outdated
Comment on lines +20 to +25
DetailReadOnlyFieldView(
label: .module("envSet List"),
value: payload.content,
isSensitive: true,
isCopyable: true
)

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

라벨 표기를 다른 섹션과 맞추세요.

"envSet List"만 소문자로 시작합니다. 다른 섹션 라벨은 "Link String", "Certification JSON"처럼 Title Case입니다. 사용자에게 보이는 문자열이므로 표기를 통일하세요.

✏️ 제안 diff
-                label: .module("envSet List"),
+                label: .module("Env Set List"),
📝 Committable suggestion

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

Suggested change
DetailReadOnlyFieldView(
label: .module("envSet List"),
value: payload.content,
isSensitive: true,
isCopyable: true
)
DetailReadOnlyFieldView(
label: .module("Env Set List"),
value: payload.content,
isSensitive: true,
isCopyable: true
)
🤖 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/SecretDetail/Components/Sections/DetailEnvSetSectionView.swift`
around lines 20 - 25, Update the label passed to DetailReadOnlyFieldView in
DetailEnvSetSectionView from “envSet List” to Title Case “EnvSet List”, matching
the capitalization used by labels such as “Link String” and “Certification
JSON”.

- secretUpdated·deleted가 secretList(.refresh)만 보내 사이드바 개수가 그대로 남았다. .refresh는 목록만 다시 읽고 secretsChanged를 발신하지 않으며, 그 delegate는 mutationResponse 성공 경로에서만 나온다
- 즐겨찾기는 Starred, 삭제는 All·Deleted 개수를 바꾸므로 sidebar(.countsRefreshRequested)를 함께 지시
- secretCreated의 기존 선례처럼 .concatenate로 순차 실행
- 두 테스트에 countsRefreshRequested·countsResponse 수신과 date·fetchCounts 스텁 추가
- SecretDetailError: map이 조회·저장소 실패까지 .unexpected로 모으는데 복호화 문구를 보여주고 있었다. alert가 이 케이스에 쓰는 문구와 같은 값으로 분리 (기존 키 재사용, xcstrings 변경 없음)
- DVMultilineTextContainer: 프리뷰 전체를 #if DEBUG로 감쌌다. 감싸지 않아 프리뷰 문자열이 릴리즈 바이너리에 포함되고 있었다
- 프리뷰 픽스처의 개인키 형태 문자열 2곳 교체. 값이 가짜여도 시크릿 스캐너가 매번 탐지 결과를 올려 같은 지적이 반복된다
- FormLayoutMetrics.verticalPadding 추가. 세로 여백이 좌우 상수를 재사용하고 있어 좌우 조정 시 함께 끌려갔다
- 만료일 포맷터 주석 정정: timeZone 미지정으로 기기 로컬 시간대로 렌더된다는 사실과 그 이유, 자정 정규화가 생성 화면 매핑 사안이라는 점 명시

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

Q1.

  • Environment, Type 필드를 정적 라디오 필드로 하지 않은 이유가 궁금합니다
  • SSL Required 필드를 정적 체크박스 필드로 하지 않은 이유가 궁금합니다

R1.

  • OAuth Client의 Client ID, SSH&Credentials/SSL Certification의 Certificate·certificateChain은 복사 가능한 정적 텍스트 필드로 수정 부탁드립니다.

R2.

  • 암호화 Multiline 처리 필요: ServiceAccountFields.credentialJSON, SSHKeyFields.privateKey, SSLCertFields.certificate, SSLCertFields.sslPrivateKey, SSLCertFields.certificateChain, EnvSetFields.envContent, CustomFields.value

R3.

  • Copy 버튼 필요: SSHKeyFields.passphrase(암호화) / SSHKeyFields.publicKey, SSLCertFields.renewCommand(비암호화)

R4.

  • Create/Update 시 암호화 대상 값은 Secure(마스킹) TextField로: API Key Value, OAuth Client Secret, Service Account credentialJSON, Database Link String, SSH/SSL PassPhrase, Certificate, Certificate Chain, Private Key, EnvSet List, License Key, Custom Value

R5.

  • 인증 시점: SecretDetailView 진입 시엔 인증 불필요, reveal 시·Secure 필드 복사 시에만 인증

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.

Design: 시크릿 조회 화면 뷰 구현 (9개 타입별)

2 participants