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>
64 lines
3.0 KiB
Swift
64 lines
3.0 KiB
Swift
import AppKit
|
|
import Foundation
|
|
|
|
/// macOS 파일 패널 + 네이티브 다운로드 헬퍼. AppKit(NSOpenPanel/NSSavePanel) 의존이라 AppFeature
|
|
/// (맥OS UI 계층)에 둔다 — DSKit 은 크로스플랫폼 유지(향후 iOS/watchOS). 모두 @MainActor.
|
|
@MainActor
|
|
enum FilePanels {
|
|
/// 업로드할 파일 1개 선택. 취소 시 nil.
|
|
static func pickFileToUpload() -> URL? {
|
|
let panel = NSOpenPanel()
|
|
panel.allowsMultipleSelection = false
|
|
panel.canChooseDirectories = false
|
|
panel.canChooseFiles = true
|
|
panel.message = "업로드할 문서를 선택하세요"
|
|
panel.prompt = "업로드"
|
|
return panel.runModal() == .OK ? panel.url : nil
|
|
}
|
|
|
|
/// 저장 위치 선택. 취소 시 nil. 사용자가 고른 위치 = 샌드박스 쓰기 권한 부여(files.user-selected).
|
|
static func pickSaveDestination(suggestedName: String) -> URL? {
|
|
let panel = NSSavePanel()
|
|
panel.nameFieldStringValue = suggestedName
|
|
panel.message = "원본 파일을 저장할 위치"
|
|
panel.prompt = "저장"
|
|
return panel.runModal() == .OK ? panel.url : nil
|
|
}
|
|
}
|
|
|
|
/// 원본 파일 네이티브 다운로드. 인증은 URL 쿼리의 ?token= 으로만 이뤄지므로(헤더 아님), 토큰이 든
|
|
/// URL 은 절대 로깅/에러 메시지에 노출하지 않는다. 저장 위치는 사용자가 NSSavePanel 로 선택.
|
|
@MainActor
|
|
enum FileDownloader {
|
|
enum Outcome: Equatable {
|
|
case saved(URL)
|
|
case cancelled
|
|
case failed(String)
|
|
}
|
|
|
|
/// `url` = DSDownload.fileURL 로 만든 ?token= 인증 URL. `suggestedName` = 원본 파일명.
|
|
static func download(from url: URL, suggestedName: String) async -> Outcome {
|
|
guard let dest = FilePanels.pickSaveDestination(suggestedName: suggestedName) else {
|
|
return .cancelled
|
|
}
|
|
do {
|
|
let (temp, response) = try await URLSession.shared.download(from: url)
|
|
// 다운로드된 임시 파일은 호출자 책임(async download 변형은 자동삭제 안 함) — 모든 종료
|
|
// 경로에서 정리. 성공 시 move 가 temp 를 옮긴 뒤라 removeItem 은 무해한 no-op.
|
|
defer { try? FileManager.default.removeItem(at: temp) }
|
|
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
|
|
// 상태 코드만 노출 — URL/토큰은 절대 포함하지 않는다.
|
|
return .failed("다운로드 실패 (HTTP \(http.statusCode))")
|
|
}
|
|
if FileManager.default.fileExists(atPath: dest.path) {
|
|
try FileManager.default.removeItem(at: dest)
|
|
}
|
|
try FileManager.default.moveItem(at: temp, to: dest)
|
|
return .saved(dest)
|
|
} catch {
|
|
// URLError/파일 오류의 localizedDescription 엔 URL 이 포함되지 않는다.
|
|
return .failed("저장 실패: \((error as NSError).localizedDescription)")
|
|
}
|
|
}
|
|
}
|