6133eb6926
- 맥·iOS 2타깃, WKWebView 로 웹 UI 100% 재사용(2026-06-15 결정: 맥/아이폰=웹 래퍼) - 영속 쿠키(로그인 유지), 첨부 응답 다운로드 처리, 업로드는 네이티브 피커 자동 - 맥 창 off-screen 가드(분리 모니터 좌표 저장 시 중앙 복귀) - DS 초록 라운드 아이콘(맥=라운드/iOS=풀블리드, 1024px 생성) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
4.1 KiB
Swift
97 lines
4.1 KiB
Swift
import SwiftUI
|
|
import WebKit
|
|
|
|
#if os(macOS)
|
|
import AppKit
|
|
#else
|
|
import UIKit
|
|
#endif
|
|
|
|
/// document.hyungi.net 을 로드하는 WKWebView 래퍼 (맥=NSViewRepresentable / iOS=UIViewRepresentable).
|
|
/// 영속 데이터스토어 = 로그인 쿠키 유지. 첨부(Content-Disposition: attachment) 응답은 다운로드 처리.
|
|
/// 파일 업로드(file input)는 WKWebView 가 네이티브 피커로 자동 처리.
|
|
struct WebView {
|
|
let url: URL
|
|
|
|
func makeCoordinator() -> Coordinator { Coordinator() }
|
|
|
|
@MainActor
|
|
fileprivate func makeWebView(coordinator: Coordinator) -> WKWebView {
|
|
let cfg = WKWebViewConfiguration()
|
|
cfg.websiteDataStore = .default() // 영속 쿠키 → 로그인 유지(브라우저처럼)
|
|
let wv = WKWebView(frame: .zero, configuration: cfg)
|
|
wv.navigationDelegate = coordinator
|
|
wv.allowsBackForwardNavigationGestures = true
|
|
wv.load(URLRequest(url: url))
|
|
return wv
|
|
}
|
|
|
|
final class Coordinator: NSObject, WKNavigationDelegate, WKDownloadDelegate {
|
|
// 첨부 응답이면 다운로드, 아니면 일반 표시(PDF 등 인라인).
|
|
func webView(_ webView: WKWebView,
|
|
decidePolicyFor navigationResponse: WKNavigationResponse,
|
|
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
|
|
if let http = navigationResponse.response as? HTTPURLResponse,
|
|
let cd = http.value(forHTTPHeaderField: "Content-Disposition"),
|
|
cd.lowercased().contains("attachment") {
|
|
decisionHandler(.download)
|
|
} else {
|
|
decisionHandler(.allow)
|
|
}
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
|
|
download.delegate = self
|
|
}
|
|
func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
|
|
download.delegate = self
|
|
}
|
|
|
|
func download(_ download: WKDownload,
|
|
decideDestinationUsing response: URLResponse,
|
|
suggestedFilename: String) async -> URL? {
|
|
#if os(macOS)
|
|
let folder = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
|
#else
|
|
let folder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
|
#endif
|
|
let dir = folder ?? FileManager.default.temporaryDirectory
|
|
var dest = dir.appendingPathComponent(suggestedFilename.isEmpty ? "download" : suggestedFilename)
|
|
// 충돌 회피 (name_1.ext …)
|
|
let base = dest.deletingPathExtension().lastPathComponent
|
|
let ext = dest.pathExtension
|
|
var n = 1
|
|
while FileManager.default.fileExists(atPath: dest.path) {
|
|
let name = ext.isEmpty ? "\(base)_\(n)" : "\(base)_\(n).\(ext)"
|
|
dest = dir.appendingPathComponent(name); n += 1
|
|
}
|
|
return dest
|
|
}
|
|
}
|
|
}
|
|
|
|
#if os(macOS)
|
|
extension WebView: NSViewRepresentable {
|
|
func makeNSView(context: Context) -> WKWebView { makeWebView(coordinator: context.coordinator) }
|
|
func updateNSView(_ nsView: WKWebView, context: Context) {}
|
|
}
|
|
|
|
/// 창이 어느 화면과도 안 겹치면(분리된 외부모니터 좌표 저장 등) 메인 화면 중앙으로 복귀 — "창 안 뜸" 방지.
|
|
struct WindowOnScreenGuard: NSViewRepresentable {
|
|
func makeNSView(context: Context) -> NSView { OnScreenView() }
|
|
func updateNSView(_ nsView: NSView, context: Context) {}
|
|
final class OnScreenView: NSView {
|
|
override func viewDidMoveToWindow() {
|
|
super.viewDidMoveToWindow()
|
|
guard let win = window else { return }
|
|
if !NSScreen.screens.contains(where: { $0.visibleFrame.intersects(win.frame) }) { win.center() }
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
extension WebView: UIViewRepresentable {
|
|
func makeUIView(context: Context) -> WKWebView { makeWebView(coordinator: context.coordinator) }
|
|
func updateUIView(_ uiView: WKWebView, context: Context) {}
|
|
}
|
|
#endif
|