f527c63232
- 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>
125 lines
4.6 KiB
Swift
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)))
|
|
}
|
|
}
|