f1dc2e1a8d
- AppModel: AuthPhase 상태기계(checking/loggedOut/ready) + live() 팩토리 (LiveDSClient + realRouter, ask 토큰 = TokenProvider 단일 소스) + bootstrap (refresh 쿠키 무로그인 복귀, single-shot, 취소 시 재시도 복원) + login(TOTP 개행·공백 정규화) + 사용 중 세션 만료 시 loggedOut 강등 + 401 회전 후 다운로드 ?token= 사본 재동기화(guarded 깔때기) - LoginView 신규(기능 셸, 서버 host 표시, 서버 detail 메시지 노출) - RootView: 인증 게이트 + errorText 하단 배너(no-silent-fallback 가시화) - DSApp: 기본 .live(publicTLS=document.hyungi.net/api), DSAPP_FIXTURE=1 / DSAPP_DS_URL env 스위치(파싱 실패 = fail-loud, prod silent fallback 금지) - LiveDSClient.currentAccessToken() — realRouter ask 토큰 closure 용 - AppFeatureTests 신규 10건(인증 상태기계·single-shot·transport 사유·totp) 검증: swift test 82/82 green + xcodebuild .app BUILD SUCCEEDED + 라이브 negative-path(/auth/login 401·/auth/refresh 401, 본 서버 양 경로 도달). 3-렌즈 어드버서리얼 리뷰 반영(재진입 가드/transport 구분/env fail-loud/토큰 사본 동기화/만료 강등). Sources/AI 무수정(시그니처 동결 준수). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
41 lines
1.5 KiB
Swift
41 lines
1.5 KiB
Swift
import SwiftUI
|
|
import AppFeature
|
|
|
|
/// Thin @main entry: window + DI only. 기본 = 본 서버(GPU DS) 라이브 결선(AppModel.live —
|
|
/// LiveDSClient + 실 AIFabric 라우터, base 기본 publicTLS = https://document.hyungi.net/api).
|
|
/// env 스위치: DSAPP_FIXTURE=1 → 오프라인 스캐폴드(Fixture+Mock) / DSAPP_DS_URL → base 오버라이드
|
|
/// (예: http://100.110.63.63:8000/api). Feature logic lives in AppFeature, keeping the seam to a
|
|
/// future iPhone/Watch target trivial.
|
|
@main
|
|
struct DSApp: App {
|
|
@State private var model: AppModel
|
|
|
|
@MainActor
|
|
init() {
|
|
let env = ProcessInfo.processInfo.environment
|
|
let initial: AppModel
|
|
if env["DSAPP_FIXTURE"] == "1" {
|
|
initial = .preview
|
|
} else if let raw = env["DSAPP_DS_URL"] {
|
|
// dev 전용 오버라이드 — 파싱 실패 시 prod(publicTLS)로 silent fallback 금지, 즉사.
|
|
let trimmed = raw.hasSuffix("/") ? String(raw.dropLast()) : raw
|
|
guard let url = URL(string: trimmed), url.scheme != nil, url.host() != nil else {
|
|
fatalError("DSAPP_DS_URL 파싱 실패: \(raw)")
|
|
}
|
|
initial = .live(base: .custom(url))
|
|
} else {
|
|
initial = .live()
|
|
}
|
|
_model = State(initialValue: initial)
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
RootView()
|
|
.environment(model)
|
|
.frame(minWidth: 980, minHeight: 640)
|
|
}
|
|
.windowStyle(.automatic)
|
|
}
|
|
}
|