mycode/myApp/HaruDanim/Marketing/tools/frame_screenshots.swift
songyc macbook 2dc81fe0e5 feat: 버전 1.1 — 맥 일기 개방 + CSV 가져오기(프리미엄) + 스크린샷 전면 개편
맥 일기 (§6.7):
- 사이드바에 일기 탭 개방 (AppTab.sidebarCases 맥 필터 제거)
- 맥 노트 도구줄: 텍스트 우선 가로 스크롤 + '그리기' 토글(마우스·트랙패드 필기, drawingPolicy anyInput)
- 그리기↔배치 상호 배타, 일기 잠금 설정 맥 노출 — 아이패드 경로는 코드 불변(회귀 차단)

CSV 가져오기 (§6.8·프리미엄):
- IOS/Core/CSVImport.swift: RFC4180 파서·데이터 행 모양 판별(언어 무관)·계획/반영 분리·초 단위 멱등 중복 처리
- 행동 연결: uuid 우선 → 이름+방식 폴백 → 새로 생성(uuid 보존), '측정 중' 행 제외, 추가 전용
- DataImportView: 파일 선택(복수) → 미리 보기 → 확인 → 저장 1회. 무료 한도 우회 방지 위해 프리미엄 게이트
- 자가 테스트 -csvImportTest 20건 ALL PASS, 진행률 자가 테스트 회귀 ALL PASS

문서·현지화·버전:
- 도움말(일기 맥·CSV 복원)·프리미엄 기능 행·설정 문구 갱신, en/ja 42키 번역·stale 10키 정리 (missing 0)
- CLAUDE.md §1/§6/§6.7/§6.8/§8/§14, Marketing 설명 ko/en/ja·whats-new-1.1, MARKETING_VERSION 1.1(4타깃)

스크린샷 전면 재제작 (ko/en/ja):
- 아이폰 10장·아이패드 10장(신규: 기록 목록/주간 타임테이블/일기 양식/다크/목표)·워치 합성 1장
- 시간대 정합: SIMCTL_CHILD_TZ 부팅 주입(Asia/Bangkok) + -marketingTZ 프로브 검증 절차 확립(README)
- frame_screenshots.swift: 문구 5종 추가·일기/동기화 문구 맥 반영·renderWatch 합성 추가 (총 63장)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FVeduv1eNdXjk1ay4tSgBg
2026-07-29 11:30:25 +09:00

449 lines
24 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// App Store (macOS `swift Marketing/tools/frame_screenshots.swift` )
// (Marketing/Screenshots/<>/<>/NN-*.png)
// + / +
// Marketing/Screenshots/framed-<>/<>/ (ASC ) .
// .
import AppKit
// MARK: (Shared/Theme.swift )
func color(_ hex: String, _ alpha: CGFloat = 1) -> NSColor {
var v: UInt64 = 0
Scanner(string: hex.replacingOccurrences(of: "#", with: "")).scanHexInt64(&v)
return NSColor(
red: CGFloat((v >> 16) & 0xFF) / 255,
green: CGFloat((v >> 8) & 0xFF) / 255,
blue: CGFloat(v & 0xFF) / 255, alpha: alpha
)
}
let backgroundTop = color("#FAFAF6")
let backgroundBottom = color("#E7F0E9") //
let headlineColor = color("#2F6B4F") // Primary Green
let sublineColor = color("#5C6B60")
let accentGold = color("#D9A621") // Accent Yellow
// MARK:
struct Copy { let headline: [String: String]; let subline: [String: String] }
let iphoneCopies: [String: Copy] = [
"01-main": Copy(
headline: ["ko": "버튼 한 번으로 기록하는 하루", "en": "Track your day with one tap", "ja": "ワンタップで記録する一日"],
subline: ["ko": "시간 측정도, 횟수 기록도 탭 한 번", "en": "Time or counts — one tap is all it takes", "ja": "時間も回数もタップひとつ"]),
"02-goals": Copy(
headline: ["ko": "목표와 다짐으로 꾸준하게", "en": "Goals and quests keep you going", "ja": "目標とクエストで続けられる"],
subline: ["ko": "하루·주간·월간 진행률과 연속 달성", "en": "Daily, weekly, monthly progress and streaks", "ja": "日・週・月の達成率と継続日数"]),
"03-timetable": Copy(
headline: ["ko": "하루가 한눈에 보여요", "en": "Your day at a glance", "ja": "一日がひと目でわかる"],
subline: ["ko": "기록이 24시간 타임테이블 위에 그려져요", "en": "Records drawn on a 24-hour timetable", "ja": "記録が24時間のタイムテーブルに描かれる"]),
"04-stats": Copy(
headline: ["ko": "통계로 보는 나의 일주일", "en": "See your week in charts", "ja": "統計で見る私の一週間"],
subline: ["ko": "행동별·꼬리표별 차트와 합계·평균", "en": "Charts, totals and averages by action and tag", "ja": "アクション・タグ別のグラフと合計・平均"]),
"05-widgets": Copy(
headline: ["ko": "홈 화면에서 바로 시작", "en": "Start right from your Home Screen", "ja": "ホーム画面からすぐ開始"],
subline: ["ko": "실행 버튼·실시간 타이머·진행률 위젯", "en": "Run buttons, live timers and progress widgets", "ja": "実行ボタン・ライブタイマー・進捗ウィジェット"]),
"06-bubble": Copy(
headline: ["ko": "한 손에 잡히는 버블 메뉴", "en": "A bubble menu made for one hand", "ja": "片手に収まるバブルメニュー"],
subline: ["ko": "유리 구슬 탭 버튼이 엄지 반경에 펼쳐져요", "en": "Glass tab bubbles fan out under your thumb", "ja": "ガラスのタブが親指の届く範囲に広がる"]),
"08-dark": Copy(
headline: ["ko": "밤에는 다크 모드로 편안하게", "en": "Easy on the eyes in Dark Mode", "ja": "夜はダークモードで心地よく"],
subline: ["ko": "라이트·다크·시스템 설정 따름 중 선택", "en": "Light, dark, or match the system", "ja": "ライト・ダーク・システム連動から選択"]),
"09-records": Copy(
headline: ["ko": "기록은 남기고, 바로 고치고", "en": "Every record, easy to fix", "ja": "記録は残して、すぐ直せる"],
subline: ["ko": "메모와 함께 쌓이는 하루의 기록 — 탭해서 수정", "en": "Notes attached — tap any record to edit", "ja": "メモと一緒に積み重なる記録 — タップで修正"]),
"10-week": Copy(
headline: ["ko": "일주일의 흐름이 한눈에", "en": "Your week at a glance", "ja": "一週間の流れをひと目で"],
subline: ["ko": "24시간 축 위에 그려지는 주간 타임테이블", "en": "Seven days drawn on a 24-hour timetable", "ja": "24時間軸に描かれる週間タイムテーブル"]),
]
let ipadCopies: [String: Copy] = [
"01-main": Copy(
headline: ["ko": "아이패드에서 더 넓게", "en": "Bigger on iPad", "ja": "iPadでもっと広く"],
subline: ["ko": "사이드바와 넓은 그리드로 한 화면에", "en": "Sidebar navigation and a wide grid", "ja": "サイドバーと広いグリッドで一画面に"]),
"02-diary-calendar": Copy(
headline: ["ko": "하루를 남기는 일기", "en": "A diary for every day", "ja": "毎日を残す日記"],
subline: ["ko": "기분 이모지와 사진으로 채워지는 달력 — 맥에서도", "en": "A calendar of moods and photos — on Mac too", "ja": "気分の絵文字と写真で埋まるカレンダー — Macでも"]),
"03-diary-summary": Copy(
headline: ["ko": "하루 정리를 한 페이지에", "en": "Your whole day on one page", "ja": "一日のまとめを1ページに"],
subline: ["ko": "기록·목표·할 일·타임테이블까지", "en": "Records, goals, to-dos and a timetable", "ja": "記録・目標・やること・タイムテーブル"]),
"04-diary-note": Copy(
headline: ["ko": "애플 펜슬로 자유롭게", "en": "Free-form with Apple Pencil", "ja": "Apple Pencilで自由に"],
subline: ["ko": "필기·사진·도형·텍스트 — 맥에서는 마우스와 키보드로", "en": "Ink, photos, shapes and text — mouse and keyboard on Mac", "ja": "手書き・写真・図形・テキスト — Macではマウスとキーボードで"]),
"08-diary-template": Copy(
headline: ["ko": "나만의 양식 위에 쓰는 일기", "en": "Write on your own templates", "ja": "自分の用紙の上に書く日記"],
subline: ["ko": "PDF·사진 속지를 불러와 노트 배경으로", "en": "Import PDF or photo paper as page backgrounds", "ja": "PDF・写真の用紙をートの背景に"]),
"09-dark": Copy(
headline: ["ko": "밤에는 다크 모드로 편안하게", "en": "Easy on the eyes in Dark Mode", "ja": "夜はダークモードで心地よく"],
subline: ["ko": "라이트·다크·시스템 설정 따름 중 선택", "en": "Light, dark, or match the system", "ja": "ライト・ダーク・システム連動から選択"]),
"10-goals": Copy(
headline: ["ko": "목표와 다짐으로 꾸준하게", "en": "Goals and quests keep you going", "ja": "目標とクエストで続けられる"],
subline: ["ko": "하루·주간·월간 진행률과 연속 달성", "en": "Daily, weekly, monthly progress and streaks", "ja": "日・週・月の達成率と継続日数"]),
"05-timetable": Copy(
headline: ["ko": "일주일이 한 화면에", "en": "A whole week on one screen", "ja": "一週間をひと画面で"],
subline: ["ko": "24시간 축 위에 그려지는 주간 타임테이블", "en": "Your week drawn on a 24-hour timetable", "ja": "24時間軸に描かれる週間タイムテーブル"]),
"06-widgets": Copy(
headline: ["ko": "아이패드 홈 화면에도 위젯", "en": "Widgets on your iPad too", "ja": "iPadのホーム画面にもウィジェット"],
subline: ["ko": "실행·타이머·진행률·통계를 크게", "en": "Run, time, track and chart — bigger", "ja": "実行・タイマー・進捗・統計を大きく"]),
]
/// 07-sync : + (01-main)
/// iCloud ( )
let syncCopy = Copy(
headline: ["ko": "iCloud로 모든 기기가 하나로", "en": "All your devices, one with iCloud", "ja": "iCloudですべてのデバイスがひとつに"],
subline: ["ko": "아이폰·아이패드·맥, 어디서 기록해도 이어져요", "en": "Record anywhere — iPhone, iPad or Mac", "ja": "iPhone・iPad・Mac、どこで記録しても続きから"])
/// (canvas 416×496 )
let watchCopy = Copy(
headline: ["ko": "손목에서 바로 기록", "en": "Track from your wrist", "ja": "手首からすぐ記録"],
subline: ["ko": "탭 한 번, 아이폰과 자동 동기화", "en": "One tap, synced with iPhone", "ja": "ワンタップ、iPhoneと自動同期"])
struct Device {
let folder: String // Screenshots/<folder>/<locale>/
let canvas: NSSize // ASC (px)
let headSize: CGFloat
let subSize: CGFloat
let topPadding: CGFloat
let cardWidthRatio: CGFloat
let cornerRadius: CGFloat
let copies: [String: Copy]
}
let devices = [
Device(folder: "iphone-6.9", canvas: NSSize(width: 1320, height: 2868),
headSize: 82, subSize: 44, topPadding: 132, cardWidthRatio: 0.84, cornerRadius: 64, copies: iphoneCopies),
Device(folder: "ipad-13", canvas: NSSize(width: 2064, height: 2752),
headSize: 96, subSize: 50, topPadding: 150, cardWidthRatio: 0.80, cornerRadius: 44, copies: ipadCopies),
]
let locales = ["ko", "en", "ja"]
// MARK:
/// maxWidth
func fitted(_ text: String, weight: NSFont.Weight, size: CGFloat, color: NSColor, maxWidth: CGFloat) -> NSAttributedString {
var fontSize = size
while fontSize > 20 {
let para = NSMutableParagraphStyle(); para.alignment = .center
let attr = NSAttributedString(string: text, attributes: [
.font: NSFont.systemFont(ofSize: fontSize, weight: weight),
.foregroundColor: color, .paragraphStyle: para,
])
if attr.size().width <= maxWidth { return attr }
fontSize -= 2
}
let para = NSMutableParagraphStyle(); para.alignment = .center
return NSAttributedString(string: text, attributes: [
.font: NSFont.systemFont(ofSize: 20, weight: weight),
.foregroundColor: color, .paragraphStyle: para,
])
}
func render(device: Device, shot: String, locale: String, copy: Copy) -> Bool {
let fm = FileManager.default
let inPath = "Marketing/Screenshots/\(device.folder)/\(locale)/\(shot).png"
let outDir = "Marketing/Screenshots/framed-\(device.folder)/\(locale)"
let outPath = "\(outDir)/\(shot).png"
guard let shotImage = NSImage(contentsOfFile: inPath) else {
print("!! 원본 없음: \(inPath)"); return false
}
try? fm.createDirectory(atPath: outDir, withIntermediateDirectories: true)
let W = device.canvas.width, H = device.canvas.height
guard let rep = NSBitmapImageRep(
bitmapDataPlanes: nil, pixelsWide: Int(W), pixelsHigh: Int(H),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
) else { return false }
rep.size = NSSize(width: W, height: H) // 1pt = 1px
NSGraphicsContext.saveGraphicsState()
guard let ctx = NSGraphicsContext(bitmapImageRep: rep) else { return false }
NSGraphicsContext.current = ctx
// ( )
NSGradient(colors: [backgroundBottom, backgroundTop])?
.draw(in: NSRect(x: 0, y: 0, width: W, height: H), angle: 90)
// (, )
let maxTextWidth = W - 140
let head = fitted(copy.headline[locale] ?? "", weight: .bold, size: device.headSize,
color: headlineColor, maxWidth: maxTextWidth)
let sub = fitted(copy.subline[locale] ?? "", weight: .medium, size: device.subSize,
color: sublineColor, maxWidth: maxTextWidth)
var cursorTop = device.topPadding
let headRect = NSRect(x: 70, y: H - cursorTop - head.size().height, width: maxTextWidth, height: head.size().height)
head.draw(in: headRect)
cursorTop += head.size().height + 26
//
let barWidth: CGFloat = 120
accentGold.setFill()
NSBezierPath(roundedRect: NSRect(x: (W - barWidth) / 2, y: H - cursorTop - 8, width: barWidth, height: 8),
xRadius: 4, yRadius: 4).fill()
cursorTop += 8 + 26
let subRect = NSRect(x: 70, y: H - cursorTop - sub.size().height, width: maxTextWidth, height: sub.size().height)
sub.draw(in: subRect)
cursorTop += sub.size().height + 64
// ( + + )
let cardWidth = W * device.cardWidthRatio
let cardHeight = cardWidth * (shotImage.size.height / shotImage.size.width)
let cardX = (W - cardWidth) / 2
let cardY = H - cursorTop - cardHeight
let cardRect = NSRect(x: cardX, y: cardY, width: cardWidth, height: cardHeight)
let cardPath = NSBezierPath(roundedRect: cardRect, xRadius: device.cornerRadius, yRadius: device.cornerRadius)
NSGraphicsContext.current?.saveGraphicsState()
let shadow = NSShadow()
shadow.shadowColor = NSColor.black.withAlphaComponent(0.22)
shadow.shadowBlurRadius = 46
shadow.shadowOffset = NSSize(width: 0, height: -18)
shadow.set()
NSColor.white.setFill()
cardPath.fill()
NSGraphicsContext.current?.restoreGraphicsState()
NSGraphicsContext.current?.saveGraphicsState()
cardPath.addClip()
shotImage.draw(in: cardRect, from: .zero, operation: .sourceOver, fraction: 1)
NSGraphicsContext.current?.restoreGraphicsState()
color("#2F6B4F", 0.14).setStroke()
cardPath.lineWidth = 3
cardPath.stroke()
NSGraphicsContext.restoreGraphicsState()
guard let data = rep.representation(using: .png, properties: [:]) else { return false }
do {
try data.write(to: URL(fileURLWithPath: outPath))
print("OK \(outPath)")
return true
} catch {
print("!! 저장 실패 \(outPath): \(error)")
return false
}
}
// MARK: 07-sync (+ 01-main iCloud )
/// ( + ) render()
func drawCard(image: NSImage, rect: NSRect, radius: CGFloat) {
let path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
NSGraphicsContext.current?.saveGraphicsState()
let shadow = NSShadow()
shadow.shadowColor = NSColor.black.withAlphaComponent(0.22)
shadow.shadowBlurRadius = 46
shadow.shadowOffset = NSSize(width: 0, height: -18)
shadow.set()
NSColor.white.setFill()
path.fill()
NSGraphicsContext.current?.restoreGraphicsState()
NSGraphicsContext.current?.saveGraphicsState()
path.addClip()
image.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 1)
NSGraphicsContext.current?.restoreGraphicsState()
color("#2F6B4F", 0.14).setStroke()
path.lineWidth = 3
path.stroke()
}
/// ( + ). .
func drawSyncBadge(center: NSPoint, radius: CGFloat) {
let circleRect = NSRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
NSGraphicsContext.current?.saveGraphicsState()
let shadow = NSShadow()
shadow.shadowColor = NSColor.black.withAlphaComponent(0.18)
shadow.shadowBlurRadius = 24
shadow.shadowOffset = NSSize(width: 0, height: -8)
shadow.set()
NSColor.white.setFill()
NSBezierPath(ovalIn: circleRect).fill()
NSGraphicsContext.current?.restoreGraphicsState()
color("#2F6B4F", 0.14).setStroke()
let ring = NSBezierPath(ovalIn: circleRect)
ring.lineWidth = 3
ring.stroke()
guard let symbol = NSImage(systemSymbolName: "arrow.triangle.2.circlepath", accessibilityDescription: nil)?
.withSymbolConfiguration(.init(pointSize: radius, weight: .semibold)) else { return }
let tinted = NSImage(size: symbol.size, flipped: false) { rect in
symbol.draw(in: rect)
accentGold.set()
rect.fill(using: .sourceAtop)
return true
}
tinted.draw(in: NSRect(x: center.x - symbol.size.width / 2, y: center.y - symbol.size.height / 2,
width: symbol.size.width, height: symbol.size.height))
}
func renderSync(device: Device, locale: String) -> Bool {
let fm = FileManager.default
guard let phoneShot = NSImage(contentsOfFile: "Marketing/Screenshots/iphone-6.9/\(locale)/01-main.png"),
let padShot = NSImage(contentsOfFile: "Marketing/Screenshots/ipad-13/\(locale)/01-main.png") else {
print("!! 원본 없음(07-sync 소스 01-main): \(locale)"); return false
}
let outDir = "Marketing/Screenshots/framed-\(device.folder)/\(locale)"
let outPath = "\(outDir)/07-sync.png"
try? fm.createDirectory(atPath: outDir, withIntermediateDirectories: true)
let W = device.canvas.width, H = device.canvas.height
guard let rep = NSBitmapImageRep(
bitmapDataPlanes: nil, pixelsWide: Int(W), pixelsHigh: Int(H),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
) else { return false }
rep.size = NSSize(width: W, height: H)
NSGraphicsContext.saveGraphicsState()
guard let ctx = NSGraphicsContext(bitmapImageRep: rep) else { return false }
NSGraphicsContext.current = ctx
NSGradient(colors: [backgroundBottom, backgroundTop])?
.draw(in: NSRect(x: 0, y: 0, width: W, height: H), angle: 90)
// (render() )
let maxTextWidth = W - 140
let head = fitted(syncCopy.headline[locale] ?? "", weight: .bold, size: device.headSize,
color: headlineColor, maxWidth: maxTextWidth)
let sub = fitted(syncCopy.subline[locale] ?? "", weight: .medium, size: device.subSize,
color: sublineColor, maxWidth: maxTextWidth)
var cursorTop = device.topPadding
head.draw(in: NSRect(x: 70, y: H - cursorTop - head.size().height, width: maxTextWidth, height: head.size().height))
cursorTop += head.size().height + 26
let barWidth: CGFloat = 120
accentGold.setFill()
NSBezierPath(roundedRect: NSRect(x: (W - barWidth) / 2, y: H - cursorTop - 8, width: barWidth, height: 8),
xRadius: 4, yRadius: 4).fill()
cursorTop += 8 + 26
sub.draw(in: NSRect(x: 70, y: H - cursorTop - sub.size().height, width: maxTextWidth, height: sub.size().height))
cursorTop += sub.size().height + 64
// : (, ) + (, )
let phoneCanvas = device.folder.hasPrefix("iphone")
let padW = W * (phoneCanvas ? 0.72 : 0.64)
let padH = padW * (padShot.size.height / padShot.size.width)
let padX = W - padW - (phoneCanvas ? 56 : 80)
let padYTop = cursorTop + 30
let phoneW = W * (phoneCanvas ? 0.52 : 0.37)
let phoneH = phoneW * (phoneShot.size.height / phoneShot.size.width)
let phoneX: CGFloat = phoneCanvas ? 56 : 100
let phoneYTop = padYTop + (phoneCanvas ? 560 : 430)
let padRect = NSRect(x: padX, y: H - padYTop - padH, width: padW, height: padH)
let phoneRect = NSRect(x: phoneX, y: H - phoneYTop - phoneH, width: phoneW, height: phoneH)
drawCard(image: padShot, rect: padRect, radius: phoneCanvas ? 36 : 44)
drawCard(image: phoneShot, rect: phoneRect, radius: phoneCanvas ? 56 : 48)
drawSyncBadge(
center: NSPoint(x: (padRect.midX + phoneRect.midX) / 2, y: (padRect.midY + phoneRect.midY) / 2),
radius: phoneCanvas ? 64 : 80
)
NSGraphicsContext.restoreGraphicsState()
guard let data = rep.representation(using: .png, properties: [:]) else { return false }
do {
try data.write(to: URL(fileURLWithPath: outPath))
print("OK \(outPath)")
return true
} catch {
print("!! 저장 실패 \(outPath): \(error)")
return false
}
}
// MARK: (canvas 416×496 + )
func renderWatch(locale: String) -> Bool {
let fm = FileManager.default
guard let shot = NSImage(contentsOfFile: "Marketing/Screenshots/watch/\(locale)/01-main.png") else {
print("!! 원본 없음(watch): \(locale)"); return false
}
let outDir = "Marketing/Screenshots/framed-watch/\(locale)"
let outPath = "\(outDir)/01-main.png"
try? fm.createDirectory(atPath: outDir, withIntermediateDirectories: true)
let W: CGFloat = 416, H: CGFloat = 496
guard let rep = NSBitmapImageRep(
bitmapDataPlanes: nil, pixelsWide: Int(W), pixelsHigh: Int(H),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
) else { return false }
rep.size = NSSize(width: W, height: H)
NSGraphicsContext.saveGraphicsState()
guard let ctx = NSGraphicsContext(bitmapImageRep: rep) else { return false }
NSGraphicsContext.current = ctx
NSGradient(colors: [backgroundBottom, backgroundTop])?
.draw(in: NSRect(x: 0, y: 0, width: W, height: H), angle: 90)
let maxTextWidth = W - 32
let head = fitted(watchCopy.headline[locale] ?? "", weight: .bold, size: 32,
color: headlineColor, maxWidth: maxTextWidth)
let sub = fitted(watchCopy.subline[locale] ?? "", weight: .medium, size: 17,
color: sublineColor, maxWidth: maxTextWidth)
var cursorTop: CGFloat = 26
head.draw(in: NSRect(x: 16, y: H - cursorTop - head.size().height, width: maxTextWidth, height: head.size().height))
cursorTop += head.size().height + 10
accentGold.setFill()
NSBezierPath(roundedRect: NSRect(x: (W - 56) / 2, y: H - cursorTop - 4, width: 56, height: 4),
xRadius: 2, yRadius: 2).fill()
cursorTop += 4 + 10
sub.draw(in: NSRect(x: 16, y: H - cursorTop - sub.size().height, width: maxTextWidth, height: sub.size().height))
cursorTop += sub.size().height + 16
//
let cardWidth = W * 0.56
let cardHeight = cardWidth * (shot.size.height / shot.size.width)
let cardRect = NSRect(x: (W - cardWidth) / 2, y: H - cursorTop - cardHeight,
width: cardWidth, height: cardHeight)
let path = NSBezierPath(roundedRect: cardRect, xRadius: 52, yRadius: 52)
NSGraphicsContext.current?.saveGraphicsState()
let shadow = NSShadow()
shadow.shadowColor = NSColor.black.withAlphaComponent(0.28)
shadow.shadowBlurRadius = 18
shadow.shadowOffset = NSSize(width: 0, height: -7)
shadow.set()
NSColor.black.setFill()
path.fill()
NSGraphicsContext.current?.restoreGraphicsState()
NSGraphicsContext.current?.saveGraphicsState()
path.addClip()
shot.draw(in: cardRect, from: .zero, operation: .sourceOver, fraction: 1)
NSGraphicsContext.current?.restoreGraphicsState()
color("#2F6B4F", 0.18).setStroke()
path.lineWidth = 2
path.stroke()
NSGraphicsContext.restoreGraphicsState()
guard let data = rep.representation(using: .png, properties: [:]) else { return false }
do {
try data.write(to: URL(fileURLWithPath: outPath))
print("OK \(outPath)")
return true
} catch {
print("!! 저장 실패 \(outPath): \(error)")
return false
}
}
var failures = 0
for device in devices {
for (shot, copy) in device.copies.sorted(by: { $0.key < $1.key }) {
for locale in locales {
if !render(device: device, shot: shot, locale: locale, copy: copy) { failures += 1 }
}
}
}
for device in devices {
for locale in locales {
if !renderSync(device: device, locale: locale) { failures += 1 }
}
}
for locale in locales {
if !renderWatch(locale: locale) { failures += 1 }
}
print(failures == 0 ? "== 전부 완료 ==" : "== 실패 \(failures)건 ==")
exit(failures == 0 ? 0 : 1)