diff --git a/myApp/HaruDanim/Marketing/README.md b/myApp/HaruDanim/Marketing/README.md index c4086b8..f59769f 100644 --- a/myApp/HaruDanim/Marketing/README.md +++ b/myApp/HaruDanim/Marketing/README.md @@ -12,14 +12,21 @@ AppStore/ whats-new-1.0.txt 버전 1.0 '새로운 기능' subscription-info.txt 인앱 상품 3종 메타데이터 + 자동 갱신 고지 + 등록 체크리스트 Screenshots/ - iphone-6.9/{ko,en,ja}/ 아이폰 6.9" 슬롯용 (1320×2868px — iPhone 17 Pro Max 시뮬레이터) - ipad-13/{ko,en,ja}/ 아이패드 13" 슬롯용 (2064×2752px — iPad Pro 13"(M5) 시뮬레이터) + iphone-6.9/{ko,en,ja}/ 아이폰 6.9" 원본 (1320×2868px — iPhone 17 Pro Max 시뮬레이터) + ipad-13/{ko,en,ja}/ 아이패드 13" 원본 (2064×2752px — iPad Pro 13"(M5) 시뮬레이터) watch/{ko,en,ja}/ 애플워치 슬롯용 (416×496px — Series 11 46mm. 워치 앱이 있는 앱은 워치 스크린샷 필수) + framed-iphone-6.9/… ★ 업로드 권장: 홍보 문구+브랜드 배경 합성판 (같은 캔버스 크기라 ASC에 그대로) + framed-ipad-13/… ★ 업로드 권장: 〃 (원본은 예비/재합성 소스로 보관) +tools/ + frame_screenshots.swift 합성 도구 — `swift Marketing/tools/frame_screenshots.swift` (저장소 루트에서). + 문구·색은 파일 상단에서 수정, 원본을 다시 찍으면 이 도구만 재실행하면 됨 ``` - 스크린샷: 아이폰 4장 = 모음 / 목표 / 타임테이블 / 주간 통계, 아이패드 4장 = 모음(사이드바) / 일기 달력 / 하루 정리 / 펜슬 노트, 워치 1장 = 앱 첫 화면(측정 중·즐겨찾기·꼬리표) - ASC는 6.9″(아이폰)·13″(아이패드)·워치 한 세트씩 필수 — 나머지 크기는 자동 축소 적용 - 상태바는 9:41·배터리 100%로 오버라이드된 상태로 촬영 (워치 시뮬레이터는 오버라이드 미지원 — 실제 시각 표시) +- 알려진 티: iPad 상태바의 날짜 문자열은 시뮬레이터 **시스템** 언어(한국어)를 따름 — 앱 런치 인자로는 못 바꾸는 영역. 아이폰은 시간만 표시라 무관. 완벽을 원하면 시뮬 시스템 언어 변경 후 재촬영 +- 문구 오버레이 규정: Apple은 스크린샷에 마케팅 문구·배경 합성을 허용(실제 앱 화면이 반영되어 있으면 됨) — framed 세트는 이 관행을 따른 것 ## 스크린샷 재생성 방법 diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/01-main.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/01-main.png new file mode 100644 index 0000000..b18c3c9 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/01-main.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/02-diary-calendar.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/02-diary-calendar.png new file mode 100644 index 0000000..317b79c Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/02-diary-calendar.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/03-diary-summary.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/03-diary-summary.png new file mode 100644 index 0000000..55a3c10 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/03-diary-summary.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/04-diary-note.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/04-diary-note.png new file mode 100644 index 0000000..18d8185 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/en/04-diary-note.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/01-main.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/01-main.png new file mode 100644 index 0000000..239a0e8 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/01-main.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/02-diary-calendar.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/02-diary-calendar.png new file mode 100644 index 0000000..3f64316 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/02-diary-calendar.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/03-diary-summary.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/03-diary-summary.png new file mode 100644 index 0000000..4937cc0 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/03-diary-summary.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/04-diary-note.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/04-diary-note.png new file mode 100644 index 0000000..f49a9a1 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ja/04-diary-note.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/01-main.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/01-main.png new file mode 100644 index 0000000..9406044 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/01-main.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/02-diary-calendar.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/02-diary-calendar.png new file mode 100644 index 0000000..f128597 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/02-diary-calendar.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/03-diary-summary.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/03-diary-summary.png new file mode 100644 index 0000000..d2232bc Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/03-diary-summary.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/04-diary-note.png b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/04-diary-note.png new file mode 100644 index 0000000..f78dbd4 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-ipad-13/ko/04-diary-note.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/01-main.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/01-main.png new file mode 100644 index 0000000..807d41d Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/01-main.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/02-goals.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/02-goals.png new file mode 100644 index 0000000..ce63a03 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/02-goals.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/03-timetable.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/03-timetable.png new file mode 100644 index 0000000..6640991 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/03-timetable.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/04-stats.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/04-stats.png new file mode 100644 index 0000000..57be317 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/en/04-stats.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/01-main.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/01-main.png new file mode 100644 index 0000000..7ecfdb1 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/01-main.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/02-goals.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/02-goals.png new file mode 100644 index 0000000..e368be9 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/02-goals.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/03-timetable.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/03-timetable.png new file mode 100644 index 0000000..0adefea Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/03-timetable.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/04-stats.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/04-stats.png new file mode 100644 index 0000000..6c85f4f Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ja/04-stats.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/01-main.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/01-main.png new file mode 100644 index 0000000..5c69844 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/01-main.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/02-goals.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/02-goals.png new file mode 100644 index 0000000..8ebe24c Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/02-goals.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/03-timetable.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/03-timetable.png new file mode 100644 index 0000000..396bdad Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/03-timetable.png differ diff --git a/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/04-stats.png b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/04-stats.png new file mode 100644 index 0000000..265dfe4 Binary files /dev/null and b/myApp/HaruDanim/Marketing/Screenshots/framed-iphone-6.9/ko/04-stats.png differ diff --git a/myApp/HaruDanim/Marketing/tools/frame_screenshots.swift b/myApp/HaruDanim/Marketing/tools/frame_screenshots.swift new file mode 100644 index 0000000..02262b6 --- /dev/null +++ b/myApp/HaruDanim/Marketing/tools/frame_screenshots.swift @@ -0,0 +1,196 @@ +// 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": "アクション・タグ別のグラフと合計・平均"]), +] + +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 that fills with your moods", "ja": "気分の絵文字で埋まるカレンダー"]), + "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 your own templates", "ja": "手書き・写真・図形・自分の用紙まで"]), +] + +struct Device { + let folder: String // Screenshots///… + 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 + } +} + +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 } + } + } +} +print(failures == 0 ? "== 전부 완료 ==" : "== 실패 \(failures)건 ==") +exit(failures == 0 ? 0 : 1)