Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "github.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "google.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "kakaotalk.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "original"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "naver.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@ struct DVVaultContainerPreviewView: View {

@State private var selectedIndex: Int? = nil

private let vaults: [(String, String, DVVaultContainer.TrailingIcon?)] = [
("내가 설정한 이름", "2026.04.01", nil),
("이름이름이름", "2026.04.01", nil),
("Example", "2026.03.27", nil),
("LongLongNameExampleVault","2026.04.01", nil),
("만료 임박 항목", "2026.04.01", .expiringSoon),
("이름이름이름", "2026.04.01", .expiringSoon),
("만료된 항목", "2026.03.27", .expired),
("LongLongNameExampleVault","2025.04.01", .expired),
/// `typeIcon`은 DVDesign이 알 필요 없는 실제 secretType과 무관한 값이라, 폴백 렌더링 확인용으로 하나만 재사용한다.
private let placeholderTypeIcon = Image(systemName: "key.fill")

private let vaults: [(String, String, DVVaultContainer.TrailingIcon?, String?, Bool)] = [
("내가 설정한 이름", "2026.04.01", nil, nil, true),
("이름이름이름", "2026.04.01", nil, nil, true),
("Example", "2026.03.27", nil, nil, true),
("LongLongNameExampleVault","2026.04.01", nil, nil, true),
("Google 계정", "2026.04.01", nil, "google", false),
("GitHub 계정", "2026.04.01", nil, "github", false),
("네이버 계정", "2026.04.01", nil, "naver", false),
("카카오톡 계정", "2026.04.01", nil, "kakaotalk", false),
("만료 임박 항목", "2026.04.01", .expiringSoon, "google", false),
("이름이름이름", "2026.04.01", .expiringSoon, nil, true),
("만료된 항목", "2026.03.27", .expired, "github", false),
("LongLongNameExampleVault","2025.04.01", .expired, nil, true),
]

var body: some View {
Expand Down Expand Up @@ -60,6 +67,8 @@ extension DVVaultContainerPreviewView {
DVVaultContainer(
name: vaults[index].0,
date: vaults[index].1,
service: vaults[index].3,
typeIcon: vaults[index].4 ? placeholderTypeIcon : nil,
trailingIcon: vaults[index].2,
isSelected: selectedIndex == index
)
Expand Down
68 changes: 68 additions & 0 deletions Projects/DVDesign/Sources/Components/DVVaultContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public struct DVVaultContainer: View {

public let name: String
public let date: String
public let service: String?
/// `service`가 비어 있을 때만 쓰이는 최종 폴백. 호출부가 자신의 타입 분류에 맞는 SF Symbol 등을 넘긴다.
public let typeIcon: Image?
public let trailingIcon: TrailingIcon?
public let isSelected: Bool

Expand All @@ -25,11 +28,15 @@ public struct DVVaultContainer: View {
public init(
name: String,
date: String,
service: String? = nil,
typeIcon: Image? = nil,
trailingIcon: TrailingIcon? = nil,
isSelected: Bool = false
) {
self.name = name
self.date = date
self.service = service
self.typeIcon = typeIcon
self.trailingIcon = trailingIcon
self.isSelected = isSelected
}
Expand All @@ -52,10 +59,71 @@ public struct DVVaultContainer: View {

extension DVVaultContainer {

@ViewBuilder
private var avatarCircle: some View {
if let logo = ServiceLogoCatalog.logo(forService: service) {
logoAvatar(logo)
} else if let serviceInitial {
initialAvatar(serviceInitial)
} else {
typeIconAvatar
}
}

private func logoAvatar(_ logo: DVServiceLogo) -> some View {
Circle()
.fill(logo.brandColor)
.frame(width: 44, height: 44)
.overlay {
if logo.rendersAsTemplate {
Image(logo.assetName, bundle: .module)
.renderingMode(.template)
.resizable()
.scaledToFit()
.foregroundStyle(Color.dv(.white))
.frame(width: 22, height: 22)
} else {
// 배경+글리프가 한 path로 합쳐진 에셋: template 틴트를 걸지 않고 원본 색상 그대로 그린다.
Image(logo.assetName, bundle: .module)
.resizable()
.scaledToFit()
.frame(width: 22, height: 22)
}
}
}

private func initialAvatar(_ initial: String) -> some View {
Circle()
.fill(Color.dv(.gray300))
.frame(width: 44, height: 44)
.overlay(
Text(initial)
.dvFont(.headingLG)
.foregroundStyle(Color.dv(.gray600))
)
}

@ViewBuilder
private var typeIconAvatar: some View {
Circle()
.fill(Color.dv(.gray300))
.frame(width: 44, height: 44)
.overlay {
if let typeIcon {
typeIcon
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color.dv(.gray600))
}
}
}

/// 로고 매칭에 실패했지만 `service` 자체는 채워져 있을 때 쓰는 중간 폴백.
private var serviceInitial: String? {
guard let service else { return nil }
guard let first = service.trimmingCharacters(in: .whitespacesAndNewlines).first else { return nil }
return String(first).uppercased()
}

private var textStack: some View {
Expand Down
14 changes: 14 additions & 0 deletions Projects/DVDesign/Sources/Foundations/Color/Color+Hex.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright © 2026 Devault. All rights reserved

import SwiftUI

extension Color {
/// 0xRRGGBB 형식의 브랜드 컬러 하드코딩(예: 서비스 로고 색상)에 쓴다.
init(hex: UInt32) {
self.init(
red: Double((hex >> 16) & 0xFF) / 255,
green: Double((hex >> 8) & 0xFF) / 255,
blue: Double(hex & 0xFF) / 255
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright © 2026 Devault. All rights reserved

import SwiftUI

public struct DVServiceLogo {
public let assetName: String
public let brandColor: Color
/// `false`면 SVG가 배경과 글리프를 한 path로 합쳐 그린다(예: KakaoTalk의 사각 배경+말풍선).
/// 이런 에셋은 `.template` 틴트를 걸면 배경까지 단색으로 뭉개지므로 원본 색상 그대로 그려야 한다.
public let rendersAsTemplate: Bool
}

/// `Secret.service` 자유 텍스트를 정규화해 번들된 브랜드 로고를 찾는다.
/// 매칭되는 항목이 없으면 `nil`을 반환하며, 호출부는 이니셜 플레이스홀더로 폴백해야 한다.
public enum ServiceLogoCatalog {

public static func logo(forService service: String?) -> DVServiceLogo? {
guard let service else { return nil }
return logosByAlias[normalize(service)]
}

/// 영문/숫자만 남기고 비교한다. "Google Cloud"처럼 알려지지 않은 표기는
/// `aliases`에 미리 추가해두지 않는 한 조용히 매칭 실패해 이니셜 폴백으로 빠진다.
private static func normalize(_ raw: String) -> String {
raw.lowercased().filter { $0.isLetter || $0.isNumber }
}
Comment on lines +22 to +26

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

# Locate and inspect the target file and relevant symbol usages without executing repo code.
if [ -f Projects/DVDesign/Sources/Foundations/ServiceLogo/ServiceLogoCatalog.swift ]; then
  echo "== file exists =="
  wc -l Projects/DVDesign/Sources/Foundations/ServiceLogo/ServiceLogoCatalog.swift
  echo "== outline =="
  ast-grep outline Projects/DVDesign/Sources/Foundations/ServiceLogo/ServiceLogoCatalog.swift || true
  echo "== relevant lines =="
  nl -ba Projects/DVDesign/Sources/Foundations/ServiceLogo/ServiceLogoCatalog.swift | sed -n '1,140p'
else
  echo "target file not found"
  fd -a 'ServiceLogoCatalog.swift' .
fi

echo "== related ServiceLogo references =="
rg -n "normalize|aliases|ServiceLogo|ServiceLogoCatalog|isASCII|isLetter|isNumber" Projects/DVDesign -g '*.swift' || true

echo "== Swift/unicode properties probe =="
python3 - <<'PY'
# Read-only semantic probe: inspect how Swift Unicode scalar properties are commonly documented
# and demonstrate equivalent Python-style Unicode is_alnum split behavior, without running repo code.
samples = {
    "Google Cloud": ["Google Cloud"],
    "3D": ["3D"],
    "Cafe": ["Cafe"],
    "Zürich": ["Zürich"],
    "1ø1": ["1ø1"],
    "α": ["α"],
    "Ω": ["Ω"],
}
print("In Swift Character.isLetter/isNumber, Unicode letters/digits are included, e.g.:")
for sample, chars in samples.items():
    print(f"{sample!r}: {' '.join(f\"'{c}'={ord(c):04X}\" for c in sample)}")
PY

Repository: DevaultProject/Devault-macOS

Length of output: 514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file =="
cat -n Projects/DVDesign/Sources/Foundations/ServiceLogo/ServiceLogoCatalog.swift

echo "== related ServiceLogo references =="
rg -n "normalize|aliases|ServiceLogo|ServiceLogoCatalog|isASCII|isLetter|isNumber" Projects/DVDesign -g '*.swift' || true

echo "== Swift/unicode properties probe =="
python3 - <<'PY'
samples = {
    "Google Cloud": ["Google Cloud"],
    "3D": ["3D"],
    "Cafe": ["Cafe"],
    "Zürich": ["Zürich"],
    "1ø1": ["1ø1"],
    "α": ["α"],
    "Ω": ["Ω"],
}
print("Examples of Unicode letters/digits that String.CharacterSet.letters/digits would include:")
for sample, chars in samples.items():
    print(f"{sample!r}: {' '.join(f\"'{c}'={ord(c):04X}\" for c in sample)}")
PY

Repository: DevaultProject/Devault-macOS

Length of output: 5826


정규화 규칙과 구현의 문자 범위를 일치시키세요.

주석은 영문/숫자만 남긴다고 설명하지만 isLetter/isNumber는 Unicode 문자와 숫자도 통과시킵니다. 현재 alias가 영문 ASCII로 정의되어 있다면 영어+숫자로 엄격히 필터링하거나, Unicode alias가 허용된다고 주석과 alias 정책을 함께 명시하세요.

수정 예시
-        raw.lowercased().filter { $0.isLetter || $0.isNumber }
+        raw.lowercased().filter { $0.isASCII && ($0.isLetter || $0.isNumber) }
📝 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
/// 영문/숫자만 남기고 비교한다. "Google Cloud"처럼 알려지지 않은 표기는
/// `aliases`에 미리 추가해두지 않는 한 조용히 매칭 실패해 이니셜 폴백으로 빠진다.
private static func normalize(_ raw: String) -> String {
raw.lowercased().filter { $0.isLetter || $0.isNumber }
}
/// 영문/숫자만 남기고 비교한다. "Google Cloud"처럼 알려지지 않은 표기는
/// `aliases`에 미리 추가해두지 않는 한 조용히 매칭 실패해 이니셜 폴백으로 빠진다.
private static func normalize(_ raw: String) -> String {
raw.lowercased().filter { $0.isASCII && ($0.isLetter || $0.isNumber) }
}
🤖 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/Foundations/ServiceLogo/ServiceLogoCatalog.swift`
around lines 22 - 26, Update ServiceLogoCatalog.normalize to make its filtering
behavior match the documented and alias-supported character range: retain only
ASCII English letters and digits, or consistently revise the comment and alias
policy to support Unicode. Prefer the smallest change that preserves the
existing ASCII alias behavior.


private struct Entry {
let logo: DVServiceLogo
let aliases: [String]
}

private static let entries: [Entry] = [
Entry(
logo: DVServiceLogo(assetName: "google", brandColor: Color(hex: 0x4285F4), rendersAsTemplate: true),
aliases: ["google", "gmail", "googleworkspace"]
),
Entry(
logo: DVServiceLogo(assetName: "github", brandColor: Color(hex: 0x181717), rendersAsTemplate: true),
aliases: ["github"]
),
Entry(
logo: DVServiceLogo(assetName: "naver", brandColor: Color(hex: 0x03C75A), rendersAsTemplate: true),
aliases: ["naver"]
),
Entry(
logo: DVServiceLogo(assetName: "kakaotalk", brandColor: Color(hex: 0xFFCD00), rendersAsTemplate: false),
aliases: ["kakao", "kakaotalk"]
),
]

private static let logosByAlias: [String: DVServiceLogo] = entries.reduce(into: [:]) { result, entry in
for alias in entry.aliases {
result[alias] = entry.logo
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright © 2026 Devault. All rights reserved

import Foundation

import DVDomain

// MARK: - Preview Fixtures

extension Secret {
static let preview = Secret(
id: UUID(),
name: "GitHub API Key",
secretType: .apiKeyToken,
service: "github",
createdAt: .now,
updatedAt: .now,
payload: SecretPayload(encryptedData: Data(), keyTag: "preview", schemaVersion: 1)
)
}

extension [Secret] {
/// 아바타 폴백 3단계(로고 매칭 → service 첫 글자 → secretType 아이콘)를
/// 타입 6종에 걸쳐 전부 확인하기 위한 프리뷰 픽스처.
public static let preview: [Secret] = previewFixtures.map(\.secret)

/// `.project` 컬렉션 필터링용. `Secret`엔 프로젝트 소속 필드가 없어(실제로는 별도 조인 테이블) 프리뷰에서만 매핑을 들고 있는다.
public static func preview(in projectID: Project.ID) -> [Secret] {
previewFixtures
.filter { $0.secret.deletedAt == nil && $0.projectIDs.contains(projectID) }
.map(\.secret)
}

private static let previewFixtures: [(secret: Secret, projectIDs: Set<UUID>)] = {
let cheerLotID = [Project].preview[0].id

return [
// 1) 로고 매칭
(.preview, [cheerLotID]), // GitHub API Key · service: "github"
(secret(name: "Google 계정", type: .oauth, service: "google"), [cheerLotID]),
(secret(name: "네이버 계정", type: .oauth, service: "naver"), []),
(secret(name: "카카오톡 계정", type: .oauth, service: "kakaotalk"), []),

// 2) 매칭 실패, service 첫 글자로 폴백
(secret(name: "Stripe API Key", type: .apiKeyToken, service: "Stripe"), []),
(secret(name: "CheerLot DB", type: .database, service: "CheerLot"), [cheerLotID]),

// 3) service 없음, secretType 아이콘으로 폴백
(secret(name: "이름 없는 API 키", type: .apiKeyToken, expiresAt: .now.addingTimeInterval(-86_400)), []),
(secret(name: "사내 SSO", type: .oauth), []),
(secret(name: "운영 DB", type: .database, expiresAt: .now.addingTimeInterval(3 * 86_400)), []),
(secret(name: "배포 서버 SSH 키", type: .sshAndCredentials, expiresAt: .now.addingTimeInterval(20 * 86_400)), []),
(secret(name: ".env 모음", type: .environmentVariableSet), []),
(secret(name: "라이선스 키", type: .etc), []),

// 4) Deleted 컬렉션 전용
(secret(name: "삭제된 API 키", type: .apiKeyToken, deletedAt: .now), []),

// 5) Star(liked) 컬렉션 전용
(secret(name: "즐겨찾는 API 키", type: .apiKeyToken, liked: true), []),
]
}()

private static func secret(
name: String,
type: SecretType,
service: String? = nil,
expiresAt: Date? = nil,
liked: Bool = false,
deletedAt: Date? = nil
) -> Secret {
Secret(
id: UUID(),
name: name,
secretType: type,
service: service,
expiresAt: expiresAt,
liked: liked,
deletedAt: deletedAt,
createdAt: .now,
updatedAt: .now,
payload: SecretPayload(encryptedData: Data(), keyTag: "preview", schemaVersion: 1)
)
}
}

extension [Project] {
public static let preview: [Project] = [
Project(id: UUID(), name: "CheerLot", createdAt: .now, updatedAt: .now),
Project(id: UUID(), name: "Mobile", createdAt: .now, updatedAt: .now),
]
}
Loading