Files
hyungi_document_server/clients/ds-app/Sources/AppFeature/Shell/Components.swift
T
hyungi f527c63232 feat(ds-app): macOS 앱 마무리 — 업로드·다운로드·로그아웃 + 4섹션 페이지
- FU-C 멀티파트 업로드(DSClient.uploadDocument + LiveDSClient 401 재시도 공유 + 툴바/상태바)
- FU-D 네이티브 다운로드(NSSavePanel + URLSession, ?token= 미노출, 임시파일 정리)
- 로그아웃(AppModel.logout 세션 전체 초기화 + 계정 메뉴)
- 셸 2-column 재구성: 질문/이드 제거, 홈 코크핏 + 문서 3-pane 컬럼 브라우저
  (인스펙터 TL;DR/핵심점/심층/불일치) + 도메인 필터 전체 load-all
- 적대 리뷰 반영(stale 401 데모션·다운로드 임시파일 정리·메모 저장 saveMemo 경유·도메인 필터 선택 정합)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:52:29 +09:00

125 lines
4.6 KiB
Swift

import SwiftUI
/// raw (/ enum ) . Sage.domainColor(raw) raw
/// raw, . .
func localizedDomain(_ raw: String?) -> String {
guard let raw, !raw.isEmpty else { return "미분류" }
// (Philosophy/Aesthetics) leaf , leaf
let leaf = raw.split(separator: "/").last.map(String.init) ?? raw
let map: [String: String] = [
"Engineering": "엔지니어링", "Industrial_Safety": "산업안전", "General": "자료실",
"Programming": "프로그래밍", "법령": "법령", "Philosophy": "철학",
]
return map[raw] ?? map[leaf] ?? leaf
}
/// / (·heavy·muted) / .
struct SectionLabel: View {
let text: String
init(_ text: String) { self.text = text }
var body: some View {
Text(text)
.font(.caption.weight(.heavy))
.textCase(.uppercase)
.kerning(0.7)
.foregroundStyle(Sage.muted)
}
}
/// (Sage.card + corner 12 + Sage.line stroke + ).
struct DashCard: ViewModifier {
var padding: CGFloat = 18
func body(content: Content) -> some View {
content
.padding(padding)
.background(Sage.card, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(Sage.line))
}
}
extension View { func dashCard(padding: CGFloat = 18) -> some View { modifier(DashCard(padding: padding)) } }
/// ( ). StatCard .
struct StatCell: View {
let value: Int
let label: String
var color: Color = Sage.ink
var body: some View {
VStack(alignment: .leading, spacing: 3) {
Text("\(value)").font(.system(size: 20, weight: .semibold)).kerning(-0.6)
.monospacedDigit().foregroundStyle(color)
Text(label).font(.caption2).foregroundStyle(Sage.muted)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct StatCard: View {
let title: String
let value: Int
let color: Color
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.caption).foregroundStyle(Sage.muted)
Text("\(value)").font(.title.weight(.bold)).foregroundStyle(color)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(14)
.background(Sage.card, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(Sage.line))
}
}
struct DomainBar: View {
let name: String
let count: Int
let max: Int
var body: some View {
HStack(spacing: 10) {
Text(name).font(.caption).foregroundStyle(Sage.ink)
.frame(width: 120, alignment: .leading).lineLimit(1)
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4).fill(Sage.surface)
RoundedRectangle(cornerRadius: 4)
.fill(Sage.domainColor(name))
.frame(width: geo.size.width * fraction)
}
}
.frame(height: 10)
Text("\(count)").font(.caption.monospacedDigit()).foregroundStyle(Sage.muted)
.frame(width: 44, alignment: .trailing)
}
}
private var fraction: CGFloat { max > 0 ? CGFloat(count) / CGFloat(max) : 0 }
}
struct ScoreBar: View {
let score: Double
var body: some View {
HStack(spacing: 6) {
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 3).fill(Sage.surface)
RoundedRectangle(cornerRadius: 3).fill(Sage.brand)
.frame(width: geo.size.width * CGFloat(min(max(score, 0), 1)))
}
}
.frame(height: 6)
Text(String(format: "%.2f", score)).font(.caption2.monospacedDigit()).foregroundStyle(Sage.muted)
}
}
}
struct ErrorBanner: View {
let text: String
var body: some View {
Text(text)
.font(.callout)
.foregroundStyle(Sage.danger)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(Sage.danger.opacity(0.08), in: RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Sage.danger.opacity(0.3)))
}
}