docs(keging): App Store 배포 자료 작성 — Marketing 폴더(설명·키워드·프로모션·새 기능·리뷰 메모 3언어 + 스크린샷 아이폰 9·워치 3 × ko/en/ja 원본·합성), 개인정보처리방침, PrivacyInfo.xcprivacy 3타깃

- Marketing/AppStore: app-name-subtitle·description-{ko,en,ja}·keywords·promotional-text·whats-new-1.0·review-notes(ASC 입력값 + App Review 영문 메모: HealthKit·백그라운드 오디오·워치)
- Marketing/Screenshots: iPhone 17 Pro Max(1320×2868) 9장 × 3언어 — 첫 화면(워치 연결됨 배지)·수축·이완·하루 통계·주간 통계·운동 세팅·다이나믹 아일랜드 확장 뷰·다크·완료 / Watch S11 46mm(416×496) 3장 × 3언어 — 시작·운동 중·세팅. framed-* = 홍보 문구 합성판(업로드 대상)
- Marketing/tools: frame_screenshots.swift(하루 다님 도구 이식, 아이폰+워치) / clean_watch_statusbar.py(워치 시뮬 '연결 끊김' 아이콘 제거)
- Marketing/README.md: 구성·재촬영 요령(시간대 정합 2단계 부팅, 아일랜드 확장 뷰는 AppleScript 메뉴+CGEvent 길게 누름, LA는 시스템 언어, 워치 아이콘 규칙)·출시 체크리스트(무료·인앱 없음)
- Docs/PRIVACY.md: 개인정보처리방침 ko/en/ja — GitHub notalentprogrammer/Keging 공개 저장소에 게시 예정(URL이 description·review-notes에 반영됨)
- PrivacyInfo.xcprivacy(IOS·워치·위젯): 수집·추적 없음, UserDefaults CA92.1·1C8F.1 — 동기화 그룹 자동 포함, 3타깃 빌드 그린·번들 포함 확인
- CLAUDE.md: §1 현재 상태(배포 자료 완료·사용자 다음 단계), §2.1 구조, §2.2 마케팅 시뮬, §2.5 마케팅 자료 유지 원칙, §8 갱신

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
This commit is contained in:
songyc macbook 2026-09-05 06:28:34 +09:00
parent 10589f7182
commit 0a25ea5ac7
88 changed files with 956 additions and 2 deletions

View File

@ -20,7 +20,7 @@
| 최소 OS | iOS 18.0 / watchOS 10.0 |
| 수익 모델 | **완전 무료** — 광고·결제 일절 없음. CloudKit 없음. HealthKit은 **쓰기 전용**(운동 기록 저장 — §3.7) |
| 언어 | **한국어(원문)/영어/일본어** — String Catalog(타깃별 `Localizable.xcstrings`, InfoPlist 포함). 설정 → 언어(시스템/한국어/English/日本語)에서 앱 내 전환: `AppleLanguages` 오버라이드, **완전 종료 후 재실행 시 적용**(하루 다님 검증 방식). 워치·Live Activity·단축어 표기는 각자의 시스템 언어를 따름 |
| 현재 상태 (2026-09-03) | **8차 반영 완료, 재테스트 대기.** 8차(워치 수정 2건): ①기본·톡톡 진동이 너무 약함 → **.notification(알림급)으로 교체**(§3.3) ②세팅 크라운 충돌 → **목록은 스크롤 전용, 숫자 항목은 눌러서 크라운 편집 화면**(CrownValueEditor, §3.6). 7차(플랫폼 결정: 현행 iOS+워치 유지): ①폰 첫 화면 시작 버튼 위 **워치 연결 배지**(연결됨=그린 전파 아이콘/미연결·미연동=슬래시. ⚠iOS 제약상 '연결됨'은 워치에서 케깅이 켜져 있을 때만 — §4) + **미연결 시작 확인 창**(기록은 동일함을 안내, 설정 > 애플워치 토글로 끔, 단축어·autoStart는 경고 없이 통과) ②**세팅 기기별 독립** — config 동기화 전면 제거(§3.6), 워치는 시작 화면 **왼쪽 스와이프 세팅 페이지**(TabView .page: 준비/수축/이완/횟수 스테퍼 + 진동 2종 피커(선택 즉시 미리 울림) + 운동 유형·강도, 요약 라인 즉시 반영) ③**워치 진동 강화 재설계**(§3.3: .start/.stop급 강한 시퀀스). 6차: DI 컴팩트=두 색 점+남은 시간, 확장=잠금화면과 동일 실시간 현황, 통계 하루 탭 꺾은선 제거. 5차: LA 완전 자가 렌더링(무갱신 — 실기기 정상 확인). "컴팩트에 국면 글자"는 iOS 제약으로 불가 확정(서버 푸시만 가능 — 기각). **재테스트(사용자)**: 워치 세팅 페이지·강한 진동 3종+완료, 폰 배지·경고 창, 워치 기록이 폰 통계 합류 | 5차 실기기 판독: 진동은 끝까지 제때 = 앱은 백그라운드 생존, 그러나 **국면 라벨·횟수는 항상 수축 2회차(4번째 전송)에서 동결 + 진행바만 계속 흐름** → 백그라운드 LA 갱신을 시스템이 예산제로 폐기하는 것으로 확정, staleDate 재렌더도 시뮬 실측 결과 발화 안 함 → **갱신 자체를 없앤 설계로 전환**(§3.4): 시작 시 1회 등록(`ensureStarted`), 이후 update 호출 0회, 화면은 전부 시스템 렌더(타임라인 바+플레이헤드+남은 시간)와 영원히 참인 정적 정보(수축·이완 범례, 세트 구성)만. 국면 라벨·횟수 텍스트는 LA에서 제거(국면=진동+플레이헤드 색, 횟수=앱 열면). 시뮬 검증: 등록 후 갱신 없이 35초간 카운트다운 4:57→4:27 정확·미니 바 전진·완주 시 DI 해제, 3종 빌드 그린·경고 0·l10n 0/0/0. 이전 라운드: 3차(햅틱 엔진 세션 부착·키퍼 하드닝·하트비트·워치 세팅 pull·완료 진동·오늘의 타임라인·칼로리 정합), 4차(디더 노이즈·브리지 태스크·타임라인 시각 라벨 제거). **재테스트 항목(사용자, 아이폰)**: ①DI 컴팩트(미니 타임라인+남은 시간)·확장·잠금화면 배너(범례+큰 남은 시간+바+세트 구성)가 세트 끝까지 흐르는지 — 이제 갱신이 없어 구조적으로 동결 불가 ②완료 진동·백그라운드 진동(이미 정상 확인됨)·워치 세팅 동기화·칼로리 |
| 현재 상태 (2026-09-05) | **App Store 배포 자료 작성 완료(2026-09-05) — 사용자 등록 단계 대기.** `Marketing/`(하루 다님 구성 이식: 이름·부제목/설명/키워드/프로모션/새 기능/리뷰 메모 3언어 + 스크린샷 아이폰 9장·워치 3장 × ko/en/ja 원본+합성, 합성 도구·재촬영 요령·출시 체크리스트는 `Marketing/README.md`) + `Docs/PRIVACY.md`(개인정보처리방침 3언어) + `PrivacyInfo.xcprivacy` 3타깃(ITMS-91053 대응, 코드 무변경). **사용자 다음 단계**: ①GitHub 공개 저장소 `notalentprogrammer/Keging`에 PRIVACY.md 게시(URL이 description·review-notes에 박혀 있음) ②ASC 앱 생성·자료 입력(review-notes.txt) ③Archive → 업로드 → TestFlight → 제출. 심사 예상 쟁점: 백그라운드 오디오 모드(2.5.4) — 리뷰 메모에 용도 소명 문구 준비됨. 코드·버전(1.0/빌드 1) 무변경. 이전: **8차 반영 완료, 재테스트 대기.** 8차(워치 수정 2건): ①기본·톡톡 진동이 너무 약함 → **.notification(알림급)으로 교체**(§3.3) ②세팅 크라운 충돌 → **목록은 스크롤 전용, 숫자 항목은 눌러서 크라운 편집 화면**(CrownValueEditor, §3.6). 7차(플랫폼 결정: 현행 iOS+워치 유지): ①폰 첫 화면 시작 버튼 위 **워치 연결 배지**(연결됨=그린 전파 아이콘/미연결·미연동=슬래시. ⚠iOS 제약상 '연결됨'은 워치에서 케깅이 켜져 있을 때만 — §4) + **미연결 시작 확인 창**(기록은 동일함을 안내, 설정 > 애플워치 토글로 끔, 단축어·autoStart는 경고 없이 통과) ②**세팅 기기별 독립** — config 동기화 전면 제거(§3.6), 워치는 시작 화면 **왼쪽 스와이프 세팅 페이지**(TabView .page: 준비/수축/이완/횟수 스테퍼 + 진동 2종 피커(선택 즉시 미리 울림) + 운동 유형·강도, 요약 라인 즉시 반영) ③**워치 진동 강화 재설계**(§3.3: .start/.stop급 강한 시퀀스). 6차: DI 컴팩트=두 색 점+남은 시간, 확장=잠금화면과 동일 실시간 현황, 통계 하루 탭 꺾은선 제거. 5차: LA 완전 자가 렌더링(무갱신 — 실기기 정상 확인). "컴팩트에 국면 글자"는 iOS 제약으로 불가 확정(서버 푸시만 가능 — 기각). **재테스트(사용자)**: 워치 세팅 페이지·강한 진동 3종+완료, 폰 배지·경고 창, 워치 기록이 폰 통계 합류 | 5차 실기기 판독: 진동은 끝까지 제때 = 앱은 백그라운드 생존, 그러나 **국면 라벨·횟수는 항상 수축 2회차(4번째 전송)에서 동결 + 진행바만 계속 흐름** → 백그라운드 LA 갱신을 시스템이 예산제로 폐기하는 것으로 확정, staleDate 재렌더도 시뮬 실측 결과 발화 안 함 → **갱신 자체를 없앤 설계로 전환**(§3.4): 시작 시 1회 등록(`ensureStarted`), 이후 update 호출 0회, 화면은 전부 시스템 렌더(타임라인 바+플레이헤드+남은 시간)와 영원히 참인 정적 정보(수축·이완 범례, 세트 구성)만. 국면 라벨·횟수 텍스트는 LA에서 제거(국면=진동+플레이헤드 색, 횟수=앱 열면). 시뮬 검증: 등록 후 갱신 없이 35초간 카운트다운 4:57→4:27 정확·미니 바 전진·완주 시 DI 해제, 3종 빌드 그린·경고 0·l10n 0/0/0. 이전 라운드: 3차(햅틱 엔진 세션 부착·키퍼 하드닝·하트비트·워치 세팅 pull·완료 진동·오늘의 타임라인·칼로리 정합), 4차(디더 노이즈·브리지 태스크·타임라인 시각 라벨 제거). **재테스트 항목(사용자, 아이폰)**: ①DI 컴팩트(미니 타임라인+남은 시간)·확장·잠금화면 배너(범례+큰 남은 시간+바+세트 구성)가 세트 끝까지 흐르는지 — 이제 갱신이 없어 구조적으로 동결 불가 ②완료 진동·백그라운드 진동(이미 정상 확인됨)·워치 세팅 동기화·칼로리 |
## 2. 빌드·검증 환경
@ -48,6 +48,7 @@ IOS/ # iOS 앱 타깃 (Keging)
Info.plist # UIBackgroundModes=audio (GENERATE_INFOPLIST_FILE와 병합)
Localizable.xcstrings # ko(원문)/en/ja 문자열 카탈로그 (워치·위젯 폴더에도 각자 있음)
InfoPlist.xcstrings # CFBundleDisplayName 케깅/Keging (워치 폴더에도 있음)
PrivacyInfo.xcprivacy # 개인정보 매니페스트 — 수집·추적 없음, UserDefaults CA92.1·1C8F.1 (워치·위젯 폴더에도 각자 있음, 동기화 그룹이라 자동 포함)
Shared/ # 세 타깃 공용 (pbxproj fileSystemSynchronizedGroups에 3타깃 모두 등록)
Theme.swift # 하루 다님 팔레트 (§5), Color(hex:)·Color(light:dark:)
AppGroup.swift # App Group defaults 단일 인스턴스 + 컨테이너 URL
@ -66,6 +67,8 @@ KegingWatch Watch App/ # 워치 앱 타깃
RuntimeSessionManager.swift # 확장 런타임 세션 (physical-therapy)
Info.plist # WKBackgroundModes=workout-processing·physical-therapy + 건강 문구
DesignAssets/app-icon.svg # 아이콘 원본 (에셋 PNG는 qlmanage 1024 렌더 → sips 알파 제거)
Marketing/ # App Store 등록 자료 (§2.5) — AppStore/*.txt 3언어, Screenshots/{iphone-6.9,watch,framed-*}/{ko,en,ja}, tools/frame_screenshots.swift
Docs/PRIVACY.md # 개인정보처리방침 ko/en/ja — GitHub 공개 저장소(notalentprogrammer/Keging)에 올려 ASC URL로 등록
```
### 2.2 빌드 커맨드·시뮬레이터
@ -81,6 +84,7 @@ xcodebuild -project Keging.xcodeproj -scheme "KegingWatch Watch App" \
-destination 'platform=watchOS Simulator,id=<WATCH_SIM_ID>' build
```
- **마케팅 스크린샷용(페어링됨)**: iPhone 17 Pro Max(26.5) `9142EFF2-892B-4C87-B5A2-937527A3E3A2` ↔ Watch S11 46mm(26.5) `B3B2F7F0-0414-4170-834B-E820BC4F2595`(폰 배지 '연결됨' 촬영용), 워치 단독 촬영은 비페어링 S11 46mm(26.4) `1F8C316E-D98F-4858-A66E-4B2C31A36DDC`(워치 시뮬 상태바의 빨간 '폰 연결 끊김' 아이콘 규칙·제거 도구는 `Marketing/README.md`)
- 자주 쓰는 시뮬레이터: iPhone 17 Pro(iOS 26) `A9F3446B-E1A6-4778-9062-DFEE84746245`, **iOS 18.5 하위 OS QA용** iPhone 16 Pro `C23050D5-9F7B-404D-ACC8-E263FDA0A395`, 워치 S11 46mm(26.0) `42220F37-2385-4F0D-9FA0-19864B356DC9`, **최소형 잘림 검증용** SE 40mm(11.5) `37F61FFF-D6C9-4340-A9B5-3F40C2452127`
- ⚠️ (하루 다님 교훈 계승) xcodebuild 동시 2개 금지(직렬로), **SourceKit 인라인 진단은 오탐 노이즈 — 판정은 xcodebuild만 신뢰**, DEBUG 런치 인자는 terminate 후 콜드 스타트로
- ⚠️**스킴은 공유 스킴으로 git에 봉인됨**(`Keging.xcodeproj/xcshareddata/xcschemes/` 3종, 2026-09-02) — 한때 사용자 로컬(xcuserdata) 스킴이었는데 Xcode 측 스킴 정리 중 Keging 스킴이 통째로 사라지고 워치 스킴 실행 대상이 뒤엉킨 사고가 있었다(타깃은 무사 — pbxproj는 무관). Xcode에서 스킴이 안 보이면: Xcode 재시작 → 그래도 없으면 `git restore 'Keging.xcodeproj/xcshareddata'`. **CLI 빌드 세션 중에는 Xcode를 닫아두는 것이 안전**(같은 DerivedData·프로젝트를 동시에 만지면 상태 꼬임)
@ -109,6 +113,12 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${app_files[@]
- 용어 통일: 수축=Squeeze/締める, 이완=Relax/ゆるめる, 세트=Set/セット, 횟수=Reps/回数. 말투는 ko ~해요체 / ja です·ます체
- ⚠️ **사용자 문구에 ASCII 물결표(~) 금지** — SwiftUI Text가 마크다운으로 해석해 `~쌍~`이 취소선이 된다(도움말 "1~60초" 실사고, 2026-08-29). 범위 표기는 전각 `` 사용. 그 외 마크다운 특수문자(`*`, `_`, `` ` ``)도 주의
### 2.5 마케팅 자료(App Store) 유지 원칙
- `Marketing/`는 하루 다님과 같은 구성 — **기능·문구가 바뀌면 description 3종·스크린샷도 함께 갱신**(설명의 기능 목록, 스크린샷에 보이는 화면). 재촬영·합성·ASC 입력 절차는 `Marketing/README.md`가 진실 소스
- 개인정보처리방침 URL·지원 URL은 GitHub `notalentprogrammer/Keging`(PRIVACY.md만 든 공개 저장소 — 하루 다님 방식). URL을 바꾸면 description 3종·review-notes.txt 링크도 함께
- 새 버전 제출 때: `whats-new-<버전>.txt` 추가 + 프로모션 텍스트 칸이 비어 있지 않은지 확인(하루 다님 1.5.1 전례)
## 3. 도메인·핵심 규칙
### 3.1 세트와 엔진 (`Shared/KegelEngine.swift`)
@ -196,7 +206,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${app_files[@]
## 8. 남은 작업·보류
- **실기기 테스트(사용자)**: §1 현재 상태 항목 참고. 문제 보고 시: 최소 변경 + 시뮬 재검증 + 3종 빌드 + 커밋·푸시
- App Store 출시 절차(등록·심사)는 사용자 지시 시 착수 — 등록 자료(설명·스크린샷) 미작성
- **App Store 출시(사용자 진행)**: 등록 자료는 전부 작성됨(§2.5, `Marketing/README.md` 체크리스트 ①~⑦). 남은 것은 사용자 계정 작업 — GitHub 개인정보 저장소 생성 → ASC 앱 생성·입력 → Archive/업로드 → TestFlight → 제출. 아리나 쪽 후속: 심사 피드백·거절 사유 대응, 출시 후 §1 갱신
- 하루 목표 횟수: 제외 확정(2026-08-29), 재논의 시에만
---

View File

@ -0,0 +1,156 @@
# 케깅 개인정보처리방침 / Keging Privacy Policy
- 시행일 / Effective date / 施行日: 2026-09-05
- 문의 / Contact / お問い合わせ: **songyc.eng@gmail.com**
한국어 · [English](#english) · [日本語](#日本語)
---
## 한국어
**케깅**(이하 "앱")은 사용자의 개인정보를 소중하게 생각합니다. 이 문서는 앱이 어떤 정보를 다루는지, 그 정보가 어디에 저장되는지 설명합니다.
### 1. 수집하는 개인정보: 없음
앱은 **어떠한 개인정보도 수집하지 않습니다.** 개발자가 운영하는 서버가 없으며, 계정 가입도 없습니다.
사용자가 앱에서 만드는 모든 데이터 — 운동 세팅, 완주한 세트 기록, 앱 설정 — 는 **사용자의 기기 안**(아이폰과 애플워치의 앱 전용 저장 공간)에만 저장됩니다. 애플워치에서 마친 세트 기록은 Apple의 기기 간 통신(Watch Connectivity)을 통해 사용자의 아이폰으로만 전달되며, 그 과정에 개발자나 제3자의 서버는 관여하지 않습니다.
### 2. 결제 정보: 없음
앱은 완전 무료이며 광고, 구독, 앱 내 구입이 없습니다. 따라서 어떠한 결제 정보도 다루지 않습니다.
### 3. 기기 권한의 사용
앱은 아래 권한을 **사용자가 허용한 경우에만, 명시된 용도로만** 사용합니다. 권한으로 접근한 정보는 기기 밖으로 전송되지 않습니다.
| 권한 | 용도 |
|---|---|
| 건강(HealthKit) — 쓰기 | 끝까지 마친 케겔 운동 세트를 사용자 본인의 건강 앱에 **운동**(운동 시간·유형, 선택한 경우 운동 강도, 추정 활성 에너지)으로 저장 |
| 건강(HealthKit) — 읽기 (애플워치만) | 워치에서 운동하는 동안 **심박수와 활성 에너지**를 측정해 그 운동 기록에 포함. 그 외 건강 데이터는 읽지 않음 |
건강 데이터는 특히 엄격하게 다룹니다: 앱은 읽어 온 심박수·활성 에너지를 **해당 운동 기록에 담는 용도로만** 사용하며, 이를 **서버로 전송하지 않고 앱 밖으로 내보내지 않습니다.** 건강 데이터를 광고·마케팅 등 다른 목적에 사용하지 않습니다. 건강 기록을 원치 않으면 권한 요청을 거절하거나 건강 앱 → 공유 → 앱에서 케깅의 권한을 끄면 되며, 앱의 다른 기능은 그대로 사용할 수 있습니다.
앱은 운동 중 화면이 꺼져 있어도 진동 안내와 실시간 현황(Live Activity)이 이어지도록 백그라운드 오디오 세션을 사용합니다. 이 세션은 소리를 녹음하지 않으며 마이크 권한도 사용하지 않습니다.
### 4. 제3자 제공·광고·추적: 없음
앱은 광고 SDK, 분석(애널리틱스) SDK, 추적 도구를 일절 포함하지 않으며, 어떠한 데이터도 제3자에게 제공하지 않습니다.
### 5. 데이터의 삭제
- **기기 데이터**: 앱을 삭제하면 기기(아이폰·애플워치)에 저장된 모든 데이터가 함께 삭제됩니다.
- **건강 앱 데이터**: 케깅이 저장한 운동 기록은 건강 앱에서 직접 삭제할 수 있습니다(건강 앱 → 둘러보기 → 활동 → 운동 → 해당 항목, 또는 프로파일 → 앱 → 케깅 → 앱의 모든 데이터 삭제).
### 6. 아동의 개인정보
앱은 개인정보를 수집하지 않으므로 아동의 개인정보 또한 수집하지 않습니다.
### 7. 방침의 변경
이 방침이 변경되는 경우 이 페이지를 통해 고지하며, 중요한 변경은 앱 업데이트 노트에도 안내합니다.
### 8. 문의
개인정보 관련 문의: **songyc.eng@gmail.com**
---
## English
**Keging** ("the app") respects your privacy. This document explains what information the app handles and where it is stored.
### 1. Personal data we collect: none
The app **does not collect any personal data.** There is no developer-operated server and no account sign-up.
Everything the app creates — your workout setup, records of completed sets, and app settings — is stored **only on your devices** (the app's private storage on your iPhone and Apple Watch). Records of sets completed on the Apple Watch are delivered only to your own iPhone through Apple's device-to-device Watch Connectivity; no developer or third-party server is involved.
### 2. Payments: none
The app is completely free, with no ads, subscriptions, or in-app purchases. It therefore handles no payment information.
### 3. Device permissions
The app uses the permissions below **only if you grant them, and only for the stated purpose.** Information accessed through these permissions never leaves your device.
| Permission | Purpose |
|---|---|
| Health (HealthKit) — write | Saving each fully completed Kegel set to your own Health app as a **workout** (duration and type, the effort level if you chose one, and an estimated active energy value) |
| Health (HealthKit) — read (Apple Watch only) | Measuring **heart rate and active energy** during a set on the watch so they can be included in that workout. No other health data is read |
Health data is handled with particular care: the app uses the heart rate and active energy it reads **only to include them in that workout record**, and **never sends them to a server or exports them outside the app.** Health data is never used for advertising, marketing, or any other purpose. If you prefer not to record to Health, decline the permission request or turn Keging off under Health app → Sharing → Apps; every other feature keeps working.
The app uses a background audio session so that haptic cues and the live status (Live Activity) continue while the screen is off during a set. This session does not record sound and the app does not use the microphone permission.
### 4. Third parties, ads, tracking: none
The app contains no advertising SDKs, analytics SDKs, or tracking tools, and shares no data with third parties.
### 5. Deleting your data
- **Device data**: deleting the app removes all data stored on the device (iPhone and Apple Watch).
- **Health app data**: workouts saved by Keging can be deleted directly in the Health app (Browse → Activity → Workouts → the entry, or your profile → Apps → Keging → Delete All Data from Keging).
### 6. Children's privacy
Because the app collects no personal data, it collects no personal data from children either.
### 7. Changes to this policy
Changes will be announced on this page, and significant changes will also be noted in app update notes.
### 8. Contact
Privacy inquiries: **songyc.eng@gmail.com**
---
## 日本語
**Keging**(以下「本アプリ」)は、ユーザーの個人情報を大切に扱います。本書は、本アプリがどのような情報を扱い、その情報がどこに保存されるかを説明します。
### 1. 収集する個人情報:なし
本アプリは**いかなる個人情報も収集しません。** 開発者が運営するサーバーはなく、アカウント登録もありません。
本アプリで作られるすべてのデータ — ワークアウト設定、完了したセットの記録、アプリ設定 — は、**ユーザーの端末内**iPhoneとApple Watchのアプリ専用領域にのみ保存されます。Apple Watchで完了したセットの記録は、Appleの端末間通信Watch Connectivityを通じてご自身のiPhoneにのみ届けられ、開発者や第三者のサーバーは関与しません。
### 2. 決済情報:なし
本アプリは完全無料で、広告・サブスクリプション・アプリ内課金はありません。したがって決済情報を一切扱いません。
### 3. 端末権限の使用
本アプリは、以下の権限を**ユーザーが許可した場合にのみ、明記した用途に限って**使用します。権限を通じてアクセスした情報が端末の外へ送信されることはありません。
| 権限 | 用途 |
|---|---|
| ヘルスケアHealthKit— 書き込み | 最後まで終えたケーゲル体操のセットを、ご自身のヘルスケアアプリに**ワークアウト**(時間・種類、選択した場合は運動強度、推定アクティブエネルギー)として保存 |
| ヘルスケアHealthKit— 読み取りApple Watchのみ | ウォッチでのセット中に**心拍数とアクティブエネルギー**を計測し、そのワークアウト記録に含めるため。それ以外のヘルスケアデータは読み取りません |
ヘルスケアデータはとくに厳格に扱います。本アプリは読み取った心拍数・アクティブエネルギーを**そのワークアウト記録に含める目的にのみ**使用し、**サーバーへ送信したり、アプリの外へ書き出したりすることはありません。** ヘルスケアデータを広告・マーケティングなど他の目的に使用することもありません。ヘルスケアへの記録を望まない場合は、権限の要求を拒否するか、ヘルスケアアプリ → 共有 → AppでKegingをオフにしてください。その他の機能はそのまま利用できます。
本アプリは、セット中に画面が消えていても振動ガイドとリアルタイム表示(ライブアクティビティ)が続くよう、バックグラウンドのオーディオセッションを使用します。このセッションは音声を録音せず、マイクの権限も使用しません。
### 4. 第三者提供・広告・トラッキング:なし
本アプリは広告SDK、分析SDK、トラッキングツールを一切含まず、いかなるデータも第三者に提供しません。
### 5. データの削除
- **端末のデータ**アプリを削除すると、端末iPhone・Apple Watchに保存されたすべてのデータも削除されます。
- **ヘルスケアアプリのデータ**Kegingが保存したワークアウトは、ヘルスケアアプリで直接削除できますブラウズ → アクティビティ → ワークアウト → 該当項目、またはプロフィール → App → Keging → Kegingのすべてのデータを削除
### 6. 子どもの個人情報
本アプリは個人情報を収集しないため、子どもの個人情報も収集しません。
### 7. 本ポリシーの変更
本ポリシーを変更する場合は本ページで告知し、重要な変更はアプリのアップデートノートでもお知らせします。
### 8. お問い合わせ
個人情報に関するお問い合わせ:**songyc.eng@gmail.com**

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 개인정보 매니페스트 (required reason API 선언 — ITMS-91053 대응, 하루 다님과 동일 구성)
이 앱은 데이터를 수집·추적하지 않으며, UserDefaults만 required-reason API에 해당:
CA92.1 = 앱 자신만 접근하는 설정 저장 / 1C8F.1 = 같은 App Group의 앱·확장 간 공유
(HealthKit은 required-reason API가 아니고, 건강 데이터는 사용자 본인의 건강 앱에만 쓰므로 수집 항목 없음) -->
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 개인정보 매니페스트 (required reason API 선언 — ITMS-91053 대응, 하루 다님과 동일 구성)
이 앱은 데이터를 수집·추적하지 않으며, UserDefaults만 required-reason API에 해당:
CA92.1 = 앱 자신만 접근하는 설정 저장 / 1C8F.1 = 같은 App Group의 앱·확장 간 공유
(HealthKit은 required-reason API가 아니고, 건강 데이터는 사용자 본인의 건강 앱에만 쓰므로 수집 항목 없음) -->
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 개인정보 매니페스트 (required reason API 선언 — ITMS-91053 대응, 하루 다님과 동일 구성)
이 앱은 데이터를 수집·추적하지 않으며, UserDefaults만 required-reason API에 해당:
CA92.1 = 앱 자신만 접근하는 설정 저장 / 1C8F.1 = 같은 App Group의 앱·확장 간 공유
(HealthKit은 required-reason API가 아니고, 건강 데이터는 사용자 본인의 건강 앱에만 쓰므로 수집 항목 없음) -->
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,16 @@
# App Store 앱 이름·부제목 (로케일별 각 30자 이내) — 1.0 (2026-09-05 작성, 사용자 확정 전 초안)
# ASC → 앱 → 해당 로케일 → '이름'/'부제목' 란. 이름·부제목의 단어는 자동 색인되므로 keywords.txt에서 중복 제거함.
# ※ 설치 시 홈 화면 이름(CFBundleDisplayName '케깅'/'Keging')은 그대로 — 스토어 표기만.
# ※ ASC에서 '케깅' 단독 이름이 이미 쓰이고 있으면 아래처럼 부제 붙은 형태로 등록.
[ko]
이름 (13자): 케깅 - 케겔 운동 타이머
부제목 (18자): 진동 안내·애플워치·통계·건강 기록
[en]
이름 (19자): Keging: Kegel Timer
부제목 (26자): Haptic cues, Watch & stats
[ja]
이름 (19자): Keging - ケーゲル体操タイマー
부제목 (21자): 振動ガイドの骨盤底筋トレ・Watch・統計

View File

@ -0,0 +1,45 @@
Keging — a Kegel timer that guides you by feel
Squeeze, relax, repeat. Keging runs your squeeze-and-relax cycles for the seconds and reps you set, and tells you each phase with haptics. Completely free — no ads, no purchases.
■ How it works
• One big Start button — a short countdown, then your first squeeze begins
• Squeeze while the circle on screen shrinks, and let go while it swells
• Set everything your way: ready time (010 s), squeeze and relax time (160 s each), reps (1100). The default is 10 s squeeze, 5 s relax, 20 reps — about 5 minutes per set
• Finish every rep and the set is saved with a completion screen. A set you stop midway isn't recorded
■ Haptic cues — no need to look at the screen
• Squeeze and relax each start with a different vibration, so you know what to do even with the phone in your pocket
• Three patterns (Standard, Double tap, Long buzz) to assign per phase, with a "feel it first" preview
• A long celebratory buzz when you complete a set
• Timer and haptics keep going while you use other apps or the screen is off
■ Dynamic Island and Lock Screen
• Leave the app mid-workout and a live status appears in the Dynamic Island and on the Lock Screen
• The whole set is painted as a timeline in squeeze (mint) and relax (amber) colors; a bright line advances in real time, with the remaining time beside it
■ Apple Watch
• Open the watch app and tap Start — the workout runs on your wrist, haptics included
• Phase, remaining time, and current rep on one screen
• Watch-only setup (swipe left): ready, squeeze, relax, reps, and haptic patterns tuned separately for the wrist
• Sets finished on the watch flow into your iPhone stats — even when apart, they sync automatically later
■ Statistics
• Swipe up from the main screen — daily, weekly, monthly
• Today's Timeline shows when you worked out as dots on a 24-hour axis
• Weekly and monthly tabs show reps by day with set and rep totals, switchable between calendar view (this week) and rolling view (last 7 days)
■ Apple Health and Shortcuts
• Completed sets are saved to Apple Health as workouts (choose the workout type and effort). On the watch, heart rate is recorded too
• Add the "Start Kegel Workout" action in Shortcuts to start from a Home Screen icon, Back Tap, or Siri
■ And more
• Light and dark themes, in-app language choice (한국어 · English · 日本語)
• Completely free — no ads, no purchases, no account
■ Privacy
Keging collects no personal data. Every record stays on your iPhone and Apple Watch and is never sent to any server. Apple Health entries are written only if you allow it, and only into your own Health app.
Privacy Policy: https://github.com/notalentprogrammer/Keging/blob/main/PRIVACY.md
※ Keging is an exercise timer and does not provide medical advice. If you have pain, discomfort, or a medical condition, please consult a professional first.

View File

@ -0,0 +1,45 @@
Keging — 振動で導くケーゲル体操タイマー
締めて、ゆるめる。Kegingは設定した秒数と回数のとおりに「締める・ゆるめる」のサイクルを進め、それぞれの切り替わりを振動で知らせるケーゲル体操骨盤底筋トレーニングタイマーです。広告も課金もない完全無料アプリです。
■ 使い方はかんたん
• 大きな開始ボタンひとつ — 準備のカウントダウンのあと、最初の「締める」が始まります
• 画面の円が縮むときに締め、ふくらむときに力を抜くだけ
• 準備時間0〜10秒、締める・ゆるめる時間各1〜60秒、回数1〜100回を自由に設定 — 初期値は締める10秒・ゆるめる5秒・20回で、1セット約5分
• 決めた回数をすべて終えると完了画面とともに記録が保存されます。途中で中断したセットは記録されません
■ 画面を見なくてもわかる振動ガイド
• 「締める」と「ゆるめる」の始まりで違う振動が鳴るので、ポケットに入れたままでも今どちらなのかわかります
• 3種類の振動パターン標準・トントン・ブーッを局面ごとに選び、「先に感じてみる」で確認できます
• セットを最後まで終えると長いお祝いの振動
• 他のアプリを使っていても、画面が消えていても、タイマーと振動は最後まで続きます
■ Dynamic Islandとロック画面
• ワークアウト中にアプリを離れると、Dynamic Islandとロック画面にリアルタイムの状況が表示されます
• セット全体を締める(ミント)・ゆるめる(アンバー)の色で塗ったタイムラインの上を明るいラインが実時間で進み、残り時間も一緒に見えます
■ Apple Watch
• ウォッチアプリを開いて開始をタップするだけ — 手首でそのまま進み、振動もウォッチで鳴ります
• 局面・残り時間・今何回目かをひと画面に
• ウォッチ専用の設定(左にスワイプ)— 準備・締める・ゆるめる・回数・振動パターンを手首に合わせて別々に設定
• ウォッチで終えたセットはiPhoneに届いて統計にまとまります。離れていても、あとで自動的に送られます
■ 統計
• メイン画面を上にスワイプすると統計 — 1日・週間・月間
• 1日タブの「今日のタイムライン」で、今日いつ運動したかを24時間軸の点でひと目で
• 週間・月間タブは日別の回数グラフとセット・回数の合計。「今週 ↔ 過去7日」のようにカレンダー基準と今日基準を切り替えられます
■ ヘルスケアとショートカット
• 最後まで終えたセットはヘルスケアにワークアウトとして自動記録(ワークアウトの種類・強度を選択可)。ウォッチなら心拍数も一緒に残ります
• ショートカットアプリの「ケーゲル運動を開始」アクションで、ホーム画面のアイコン・背面タップ・Siriからすぐ開始
■ そのほか
• ライト・ダークテーマ、アプリ内で言語を選択한국어・English・日本語
• 完全無料 — 広告なし、課金なし、アカウント不要
■ プライバシー
Kegingは個人情報を収集しません。すべての記録はご自身のiPhoneとApple Watchにのみ保存され、外部のサーバーへ送信されることはありません。ヘルスケアへの記録は、許可した場合にのみ、ご自身のヘルスケアアプリに書き込まれます。
プライバシーポリシー: https://github.com/notalentprogrammer/Keging/blob/main/PRIVACY.md
※ Kegingは運動タイマーであり、医学的な助言を提供するものではありません。痛みや不快感がある場合、治療中の場合は、まず専門家にご相談ください。

View File

@ -0,0 +1,45 @@
케깅 — 진동이 이끄는 케겔 운동 타이머
조이고, 풀고. 케깅은 정해 둔 시간과 횟수대로 수축·이완 사이클을 진동으로 안내하는 케겔 운동 타이머예요. 광고도 결제도 없는 완전 무료 앱입니다.
■ 이렇게 써요
• 큰 시작 버튼 하나 — 누르면 준비 카운트다운 뒤에 첫 수축이 시작돼요
• 화면의 원이 오므라들 때 조이고, 부풀어 오를 때 힘을 풀면 돼요
• 준비 시간(010초), 수축·이완 시간(각 160초), 반복 횟수(1100회)를 자유롭게 — 기본은 수축 10초·이완 5초·20회, 한 세트 약 5분
• 정한 횟수를 모두 마치면 완료 화면과 함께 기록이 저장돼요. 중간에 중단한 세트는 기록되지 않아요
■ 화면을 보지 않아도 되는 진동 안내
• 수축이 시작될 때와 이완이 시작될 때 서로 다른 진동이 울려요 — 주머니에 넣어 둔 채로도 지금 조여야 할지 풀어야 할지 알 수 있어요
• 진동 패턴 3종(기본·톡톡·지이이잉)을 국면별로 고르고, '미리 느끼기'로 확인해요
• 세트를 끝까지 마치면 긴 축하 진동이 울려요
• 다른 앱을 쓰거나 화면이 꺼져 있어도 타이머와 진동은 끝까지 이어져요
■ 다이나믹 아일랜드와 잠금화면
• 운동 중 앱을 나가면 다이나믹 아일랜드와 잠금화면에 실시간 현황이 떠요
• 세트 전체를 수축(민트)·이완(앰버) 색으로 칠한 타임라인 위로 밝은 줄이 실제 시간에 맞춰 차오르고, 남은 시간이 함께 보여요
■ 애플워치
• 워치 앱을 열고 시작 버튼을 누르면 손목에서 바로 시작 — 진동도 워치에서 울려요
• 수축·이완 상태, 남은 시간, 몇 번째인지가 한 화면에
• 워치 전용 세팅(왼쪽으로 쓸어넘기기) — 준비·수축·이완·횟수·진동 패턴을 손목에 맞게 따로 정해요
• 워치에서 마친 세트는 아이폰으로 넘어와 통계에 합쳐져요. 떨어져 있어도 나중에 자동으로 전송돼요
■ 통계
• 첫 화면을 위로 쓸어올리면 통계 — 하루·주간·월간
• 하루 탭의 '오늘의 타임라인'에서 오늘 언제 운동했는지 24시간 축 위의 점으로 한눈에
• 주간·월간 탭은 일별 횟수 그래프와 세트·횟수 합계. '이번 주 ↔ 지난 7일'처럼 달력 기준과 오늘 기준을 오갈 수 있어요
■ 애플 건강과 단축어
• 끝까지 마친 세트는 애플 건강에 운동으로 자동 기록돼요(운동 유형·강도 선택 가능). 워치에서 하면 심박수도 함께 남아요
• 단축어 앱의 '케겔 운동 시작' 동작으로 홈 화면 아이콘, 뒷면 탭, 시리에서 바로 시작할 수 있어요
■ 그 밖에
• 라이트·다크 테마, 앱 안에서 언어 선택(한국어·English·日本語)
• 완전 무료 — 광고 없음, 결제 없음, 계정 없음
■ 개인정보
케깅은 개인정보를 수집하지 않아요. 모든 기록은 사용자의 아이폰과 애플워치에만 저장되고, 외부 서버로 전송되지 않아요. 애플 건강 기록은 사용자가 권한을 허용한 경우에만, 사용자 본인의 건강 앱에 저장돼요.
개인정보처리방침: https://github.com/notalentprogrammer/Keging/blob/main/PRIVACY.md
※ 케깅은 운동 타이머일 뿐 의학적 조언을 제공하지 않아요. 통증이나 불편이 있거나 치료 중이라면 먼저 전문가와 상담하세요.

View File

@ -0,0 +1,15 @@
# App Store 키워드 (로케일별 100자 이내, 쉼표 구분·공백 없음) — 1.0 (2026-09-05 작성)
# ASC → 앱 → 해당 로케일 → '키워드' 란에 그대로 붙여넣기.
# 앱 이름·부제목의 단어(케깅/케겔/운동/타이머/진동/안내/애플워치/통계/건강/기록,
# keging/kegel/timer/haptic/cues/watch/stats, ケーゲル体操/タイマー/振動/ガイド/骨盤底筋/トレ/Watch/統計)는
# 자동 색인되므로 넣지 않는다. 붙여 쓴 복합어(케겔운동·골반저근 등)는 띄어 쓴 이름과 검색 토큰이 달라 유지.
# 의학 용어(요실금·전립선·산후 등)는 케겔 운동의 일반적 검색어라 포함 — 설명문에서는 효능을 단정하지 않는다.
[ko] (92자)
케겔운동,골반저근,골반,요실금,전립선,산후,출산후,회음부,pc근육,수축,이완,사이클,루틴,헬스,피트니스,다이나믹아일랜드,단축어,시리,워치,무료,kegel,pelvic
[en] (96자)
pelvic,floor,exercise,workout,squeeze,relax,men,women,postpartum,bladder,prostate,vibration,free
[ja] (88자)
骨盤底筋トレーニング,骨盤底,尿もれ,産後,締める,ゆるめる,運動,エクササイズ,フィットネス,健康,ヘルスケア,無料,ショートカット,バイブ,ルーティン,ウォッチ,男性,女性

View File

@ -0,0 +1,12 @@
# 프로모션 텍스트 (로케일별 170자 이내 — 심사 없이 수시 수정 가능한 문구) — 1.0 (2026-09-05 작성)
# ASC → 앱 → 해당 로케일 → '프로모션 텍스트' 란. ⚠️새 버전 초안을 만들 때마다 이 칸이 비어 있지 않은지 확인(하루 다님 1.5.1 전례).
# 첫 문장은 브랜드 멘트(진동이 이끈다), 둘째 문장이 기능 요약, 마지막이 무료 고지.
[ko] (107자)
진동이 이끄는 케겔 운동 타이머. 수축과 이완을 서로 다른 진동으로 알려 줘서 화면을 보지 않아도 돼요. 애플워치·다이나믹 아일랜드·통계·애플 건강 기록까지 — 광고도 결제도 없는 완전 무료.
[en] (167자)
A Kegel timer that guides you by feel: different haptics for squeeze and relax, so you never need to look. Apple Watch, Dynamic Island, stats, Apple Health. Free, no ads.
[ja] (104자)
振動で導くケーゲル体操タイマー。締めるとゆるめるを違う振動で知らせるので、画面を見なくても大丈夫。Apple Watch・Dynamic Island・統計・ヘルスケア記録まで — 広告も課金もない完全無料。

View File

@ -0,0 +1,39 @@
# ASC 등록 입력값 모음 + App Review 정보 메모 — 1.0 (2026-09-05 작성)
# 하루 다님 README 체크리스트(§③~⑤)를 케깅에 맞게 압축. ASC 화면 구성은 수시로 바뀌므로 항목 이름으로 찾을 것.
[앱 생성]
플랫폼 iOS / 이름 '케깅 - 케겔 운동 타이머'(app-name-subtitle.txt) / 기본 언어 한국어 / 번들 ID com.yechan.Keging / SKU keging-ios
[앱 정보]
카테고리 기본: 건강 및 피트니스 / 보조: (없음 또는 라이프스타일)
콘텐츠 권한: 제3자 콘텐츠 없음
연령 등급: 설문 전부 '없음' → 4+ (의료/치료 정보 항목은 '없음' — 앱은 운동 타이머일 뿐 의학 정보를 제공하지 않음. 최종 판단은 ASC 설문 문항 기준)
현지화: 한국어(기본)·영어(미국)·일본어 — 언어별 이름/부제목은 app-name-subtitle.txt
개인정보 처리방침 URL: https://github.com/notalentprogrammer/Keging/blob/main/PRIVACY.md
⚠️ 하루 다님과 같은 방식 — GitHub에 PRIVACY.md 하나만 든 공개 저장소를 만들어 Docs/PRIVACY.md를 올린다(저장소 생성은 사용자). URL을 바꾸면 description 3종의 링크도 함께 고칠 것
지원 URL: https://github.com/notalentprogrammer/Keging
앱 개인정보 보호: '데이터 수집 안 함' (PrivacyInfo.xcprivacy 3개 = 수집·추적 없음과 일치. 건강 데이터는 사용자 본인의 건강 앱에 쓰기만 하며 개발자에게 오지 않음)
가격: 무료 / 국가 전체 (인앱 상품 없음 → 유료 앱 계약 불필요, 무료 앱 계약만 활성이면 됨)
[버전 1.0 페이지 — 언어별]
스크린샷: iPhone 6.9″ = Screenshots/framed-iphone-6.9/<언어>/01~NN / Apple Watch = Screenshots/framed-watch/<언어>/01~03 (워치 앱 포함이라 필수)
프로모션 텍스트 = promotional-text.txt / 설명 = description-<언어>.txt / 키워드 = keywords.txt / 새로운 기능 = whats-new-1.0.txt
저작권: © 2026 Yechan Song
수출 규정: 빌드 업로드 후 '암호화' 질문이 뜨면 '아니요'(표준 암호화만 — ITSAppUsesNonExemptEncryption 미등록 상태이므로 매 빌드 질문됨. 귀찮으면 Info.plist에 ITSAppUsesNonExemptEncryption=NO 추가)
[App Review 정보]
로그인 필요 없음 체크 / 연락처 입력 / 메모(아래 영문을 그대로):
Keging is a free Kegel exercise timer with no account, no ads, and no in-app purchases.
- Tap the big Start button on the main screen to run a set (ready countdown → squeeze/relax cycles). The default set takes about 5 minutes; you can shorten it in Workout Setup (top-right slider icon), e.g. 1 s squeeze, 1 s relax, 2 reps.
- HealthKit: when a set is completed, the app saves it to Apple Health as a workout (write-only on iPhone). The Apple Watch app runs an HKWorkoutSession during the set so heart rate and active energy are included in that workout. No other health data is read, and nothing leaves the device. Permission is requested at the first start; declining simply skips the Health entry.
- Background audio mode: the app's core function is time-based haptic guidance during a set. The audio background mode keeps the workout session alive so phase-change haptics fire on time while the screen is off or another app is in front, and the Live Activity (Dynamic Island / Lock Screen) shows the live set progress. The session ends automatically when the set finishes or is stopped.
- Apple Watch: the watch app works standalone (start on the wrist, watch-only setup via left swipe). Completed sets sync to the iPhone statistics.
- Shortcuts: an App Intent "Start Kegel Workout" opens the app and starts a set.
The app provides no medical advice; a note in the description says to consult a professional in case of pain or a medical condition.
[아카이브 → 업로드 체크]
- 세 타깃 버전 일치: iOS 1.0(1) / 워치 1.0(1) / 위젯 1.0(1) — 워치 버전이 다르면 업로드 거부
- 스킴 Keging + Any iOS Device (arm64) → Product → Archive → Distribute → App Store Connect → Upload
- 재업로드는 빌드 번호 증가 필수
- TestFlight 실기기 확인: 진동 3종·완료 진동·백그라운드 진동·DI·워치 기록 합류·건강 기록

View File

@ -0,0 +1,31 @@
# 버전 1.0 '새로운 기능' (로케일별) — 2026-09-05 작성
[ko]
케깅의 첫 버전이에요.
• 수축·이완을 서로 다른 진동으로 안내하는 케겔 운동 타이머
• 준비·수축·이완 시간과 횟수, 진동 패턴 3종 설정
• 다이나믹 아일랜드·잠금화면 실시간 현황
• 애플워치 앱(손목에서 시작, 워치 전용 세팅)
• 하루·주간·월간 통계와 오늘의 타임라인
• 애플 건강 운동 기록, 단축어 '케겔 운동 시작'
• 광고·결제 없는 완전 무료
[en]
This is the first release of Keging.
• A Kegel timer that cues squeeze and relax with different haptics
• Ready, squeeze and relax times, reps, and three haptic patterns
• Live status in the Dynamic Island and on the Lock Screen
• Apple Watch app (start from your wrist, watch-only setup)
• Daily, weekly and monthly stats with Today's Timeline
• Apple Health workouts and the "Start Kegel Workout" shortcut
• Completely free — no ads, no purchases
[ja]
Kegingの最初のバージョンです。
• 締める・ゆるめるを違う振動で知らせるケーゲル体操タイマー
• 準備・締める・ゆるめる時間と回数、3種類の振動パターン
• Dynamic Island・ロック画面のリアルタイム表示
• Apple Watchアプリ手首から開始、ウォッチ専用設定
• 1日・週間・月間の統計と今日のタイムライン
• ヘルスケアへのワークアウト記録、ショートカット「ケーゲル運動を開始」
• 広告・課金なしの完全無料

View File

@ -0,0 +1,132 @@
# Marketing — App Store 등록 자료 (케깅)
ASC(App Store Connect) 등록 때 그대로 붙여넣을 자료 모음. 하루 다님 `Marketing/` 구성을 그대로 옮겨 왔다.
**앱 기능·문구가 바뀌면 이 폴더도 함께 갱신한다** — 특히 description의 기능 목록, 스크린샷에 보이는 화면.
## 구성
```
AppStore/
app-name-subtitle.txt 로케일별 이름·부제목 (각 30자 이내)
description-{ko,en,ja}.txt 앱 설명 (4000자 이내) — 로케일별 '설명' 란
keywords.txt 키워드 (로케일별 100자 이내) — '키워드' 란
promotional-text.txt 프로모션 텍스트 (170자 이내, 심사 없이 수정 가능)
whats-new-1.0.txt 버전 1.0 '새로운 기능'
review-notes.txt ASC 입력값(카테고리·URL·연령 등급) + App Review 메모(영문) + 업로드 체크
Screenshots/
iphone-6.9/{ko,en,ja}/ 아이폰 6.9″ 원본 (1320×2868px — iPhone 17 Pro Max 시뮬레이터)
watch/{ko,en,ja}/ 애플워치 원본 (416×496px — Series 11 46mm. 워치 앱이 있는 앱은 워치 스크린샷 필수)
framed-iphone-6.9/… ★ 업로드 대상: 홍보 문구+브랜드 배경 합성판 (같은 캔버스 크기라 ASC에 그대로)
framed-watch/… ★ 업로드 대상: 〃
tools/
frame_screenshots.swift 합성 도구 — `swift Marketing/tools/frame_screenshots.swift` (Keging 폴더에서).
문구·색은 파일 상단에서 수정, 원본을 다시 찍으면 이 도구만 재실행하면 됨
../Docs/PRIVACY.md 개인정보처리방침(ko/en/ja) — GitHub 공개 저장소에 올려 URL로 등록 (아래 ⚠️)
```
- 아이패드 슬롯 없음(iPhone 전용 앱, TARGETED_DEVICE_FAMILY=1). ASC는 6.9″ 아이폰 + 워치 한 세트씩 필수, 나머지 크기는 자동 축소
- **스크린샷 구성 (1.0, 2026-09-05)**:
- **아이폰 9장** = 01 첫 화면(워치 연결됨 배지) / 02 운동 중·수축 / 03 운동 중·이완 / 04 하루 통계(오늘의 타임라인) / 05 주간 통계 / 06 운동 세팅 시트 / 07 다이나믹 아일랜드 확장 뷰(홈 화면에서 길게 누름) / 08 다크 모드 / 09 완료 화면
- **워치 3장** = 01 시작 화면 / 02 운동 중 / 03 세팅 페이지
- 상태바는 9:41·배터리 100%로 오버라이드된 상태로 촬영 (워치 시뮬레이터는 오버라이드 미지원 — 실제 시각 표시)
- 문구 오버레이 규정: Apple은 스크린샷에 마케팅 문구·배경 합성을 허용(실제 앱 화면이 반영되어 있으면 됨) — framed 세트는 이 관행을 따른 것
- ⚠️ **개인정보처리방침 URL**은 하루 다님과 같은 방식 — GitHub에 `PRIVACY.md` 하나만 든 공개 저장소(`notalentprogrammer/Keging`)를 만들고 `Docs/PRIVACY.md`를 올린다(저장소 생성은 사용자 계정으로). URL이 달라지면 description 3종·review-notes의 링크를 함께 고칠 것
## 스크린샷 재생성 방법
케깅은 데모 시드가 통계뿐(`-seedStats`)이라 로케일마다 앱을 지울 필요는 없고, 실행마다 `-AppleLanguages "(xx)" -AppleLocale xx_XX`만 바꾸면 된다.
촬영 스크립트 원본은 세션 스크래치에 있었으므로 아래 요령을 기준으로 재작성한다.
```zsh
# 0) Debug 빌드 후 앱 경로 (Keging 스킴이 워치 앱까지 빌드·내장)
DD=$(ls -dt ~/Library/Developer/Xcode/DerivedData/Keging-* | head -1)
APP=$DD/Build/Products/Debug-iphonesimulator/Keging.app
PHONE=9142EFF2-892B-4C87-B5A2-937527A3E3A2 # iPhone 17 Pro Max (iOS 26.5) — 마케팅용
WATCH=B3B2F7F0-0414-4170-834B-E820BC4F2595 # Apple Watch Series 11 46mm (watchOS 26.5) — PHONE과 페어링됨(simctl pair)
# 1) 부팅 (시간대 주입 — 아래 '시간대 정합') + 상태바 오버라이드
SIMCTL_CHILD_TZ=Pacific/Auckland xcrun simctl boot $PHONE
xcrun simctl status_bar $PHONE override --time "9:41" --batteryState charged --batteryLevel 100 --wifiBars 3 --cellularBars 4
SIMCTL_CHILD_TZ=Pacific/Auckland xcrun simctl boot $WATCH
# 2) 설치 — 워치 앱은 페어링돼 있어도 자동 설치되지 않으므로 내장 번들을 직접 설치
xcrun simctl install $PHONE "$APP"
xcrun simctl install $WATCH "$APP/Watch/KegingWatch Watch App.app"
# 3) 화면별 런치 인자 (매 실행 공통: -noHealth -AppleLanguages "(ko)" -AppleLocale ko_KR / en en_US / ja ja_JP)
# ★ 폰 첫 화면의 '워치 연결됨' 배지는 워치 시뮬에서 케깅 워치 앱이 포그라운드일 때만 뜬다 — 01·08 촬영 직전에 워치 앱을 launch
# 01-main : -resetConfig (기본 세팅 10·5·20)
# 09-finished : -quickConfig -autoStart → 8초 뒤 (완주 화면. 다음 실행에 -resetConfig 필수)
# 06-setup : -resetConfig -openTimerSettings
# 02-workout : -autoStart → 약 36초 뒤 (3회차 수축, 7초 남음) / 03-relax: 같은 실행 약 45초 뒤 (3회차 이완)
# 08-dark : simctl ui $PHONE appearance dark → 기본 실행 → 촬영 → appearance light
# 05-stats-week : -seedStats -openStats -statsScope week
# 04-stats-day : -seedStats -openStats ← ⚠️ '시간대 정합' 2단계 부팅 후 촬영
# 07-dynamic-island : -autoStart 로 운동 중 → Device > Home → 아일랜드 길게 누름(확장 뷰) → 촬영 (아래 '다이나믹 아일랜드')
# 워치 01-main: 기본 / 03-settings: -openSettings / 02-workout: -autoStart → 약 36초 뒤
# 4) 합성: swift Marketing/tools/frame_screenshots.swift
```
- ⚠️ `simctl io screenshot`**기존 파일을 덮어쓰지 않음** — 재촬영 전 rm 필수
- ⚠️ `simctl launch`는 실행 중인 앱을 재시작하지 않는다 — 매번 `simctl terminate` 후 launch (DEBUG 인자는 콜드 스타트에서만 읽음)
### 시간대 정합 (상태바 9:41 ↔ 앱 안의 시각)
하루 다님 요령 계승: **부팅 시** `SIMCTL_CHILD_TZ=<IANA id>`로 주입해야 전 프로세스에 적용된다(launch 시점 주입은 유실됨).
- 일반 화면(01~03·05~09): "지금이 오전 9시대"인 시간대 — 2026-09-05 새벽(KST 05~06시) 촬영은 `Pacific/Auckland`(UTC+12 → 08~09시대)
- **하루 탭(04)만 2단계 부팅**: `-seedStats`의 오늘 세션이 08:00·13:17·21:34 고정이고 타임라인은 '지금' 뒤의 세션도 그리므로, **"지금"이 21:34 이후(밤)인 시간대로 폰을 재부팅**해 찍는다(같은 시각 KST 새벽이면 `Europe/London`(UTC+1) → 22시대). 이때 상태바는 `--time "10:41"`로 바꿔 '지금' 룰과 어긋나지 않게(자정을 넘기면 오늘 기록이 사라지니 23시대는 피할 것)
### 다이나믹 아일랜드 확장 뷰 (07) — Simulator.app 창을 마우스로 조작
simctl에는 터치 주입이 없다. `open -a Simulator`로 창을 띄운 뒤 **AppleScript(System Events)로 Device 메뉴를 클릭**하고,
**CGEvent로 마우스 클릭·길게 누름**을 보낸다(세션 스크래치의 mouse/longpress 스위프트 헬퍼 — 재작성 요령: `CGEvent(mouseEventSource:mouseType:mouseCursorPosition:mouseButton:)`로 down/up 게시).
- 창 좌표 매핑: Window > Show Device Bezels **끄기** + Window > **Pixel Accurate** → 창 position/size를 AppleScript로 읽는다(2026-09-05: position {1033,31}, size {625,1409}, 1x 디스플레이).
스크린샷 px → 맥 pt: `x = 1033 + px × (625/1320)`, `y = 31 + 51(툴바) + py × (625/1320)`. 케깅 아이콘 클릭으로 검증 후 사용
- 절차: `-autoStart` → 20초 → Device > Home(메뉴 클릭) → 아일랜드 중앙(px 660,110 → 맥 1345.5,134)을 1.2초 길게 누름 → 확장 뷰 촬영 → 아일랜드 탭으로 앱 복귀
- ⚠️ 잠금화면(Device > Lock)은 시뮬이 항상 켜짐(AOD) 형태로 그려 Live Activity의 초 단위가 "3:——"로 나와 스토어용 부적합 — 첫 잠금 때 "실시간 현황 허용" 물음창이 뜨므로(허용 버튼 클릭 필요) 굳이 잠금화면을 찍을 땐 참고
- 홈 화면 컴팩트 아일랜드(두 색 점+남은 시간)는 정상 렌더 — 필요하면 Home 직후 촬영
### 워치 상태바의 빨간 '아이폰 연결 끊김' 아이콘 (실측 2026-09-05)
- 비페어링 워치를 새로 부팅하면 앱 화면 상태바가 깨끗하지만, **워치에서 운동을 한 번 시작하면(HKWorkoutSession/런타임 세션) 그 순간부터 아이콘이 붙고 재부팅 전엔 사라지지 않는다.** 페어링 워치는 폰이 켜져 있어도 처음부터 붙어 있었다
- 그래서 **01·03(시작 화면·세팅)은 로케일마다 워치를 재부팅한 직후**(부팅 완료 + 12초 → 01 → 03) 찍고, **02(운동 중)는 아이콘이 붙은 채 찍은 뒤 `python3 Marketing/tools/clean_watch_statusbar.py Marketing/Screenshots/watch/*/02-workout.png`로 상태바 아이콘만 배경색으로 지운다**(실기기에선 실제로 끊겼을 때만 뜨는 시뮬 잔재 — 앱 내용은 무수정). 검출은 상태바 우상단 빨간 픽셀 수(도구 내부)
- `-AppleLanguages` 런치 인자로 워치 앱 문구는 바뀌지만 워치 상태바 시각은 실제 시각(오버라이드 불가)
### Live Activity(아일랜드)는 시스템 언어를 따른다
위젯 확장은 앱의 `-AppleLanguages` 인자를 받지 않으므로 en/ja 07 컷은 **시뮬 시스템 언어를 바꿔 재부팅** 후 찍는다:
`xcrun simctl spawn $PHONE defaults write "Apple Global Domain" AppleLanguages -array en` + `AppleLocale -string en_US` → shutdown/boot(TZ 주입) → **부팅 완료 후 25초 대기** → 상태바 오버라이드 → 앱을 한 번 워밍업 실행(5초)·종료 → `-autoStart` 콜드 스타트 → 18초 → Home → 길게 누름 → 촬영. 끝나면 ko로 복원. 홈 화면 앱 라벨도 함께 바뀐다.
⚠️ 부팅 직후(8초) 바로 `-autoStart`하면 Live Activity가 등록되지 않아 아일랜드가 비어 있었다(2026-09-05 실측) — 대기·워밍업이 필요
## 출시 절차 체크리스트 (케깅 — 무료·인앱 없음·CloudKit 없음)
하루 다님 README §①~⑪에서 케깅에 해당하는 것만. ⏳ = 처리 대기. 금액·세무가 걸린 내용은 ASC 안내문으로 최종 확인 권장.
### ① 계약
- [ ] 무료 앱이라 유료 앱 계약 불필요 — ASC → 비즈니스에서 **무료 앱 계약(Free Apps)** 활성 상태만 확인 (하루 다님 출시로 이미 활성)
### ② 개인정보처리방침 공개 (앱 생성 전)
- [ ] GitHub에 공개 저장소 `notalentprogrammer/Keging` 생성 → `Docs/PRIVACY.md``PRIVACY.md`로 올림 → `https://github.com/notalentprogrammer/Keging/blob/main/PRIVACY.md` 열리는지 확인
### ③ ASC 앱 생성 · 앱 정보 (review-notes.txt에 입력값 정리)
- [ ] 신규 앱: iOS / 이름 `케깅 - 케겔 운동 타이머` / 기본 언어 한국어 / 번들 ID `com.yechan.Keging` / SKU `keging-ios`
- [ ] 카테고리 **건강 및 피트니스** / 연령 등급 설문 전부 없음 → 4+ / 개인정보 URL·지원 URL 입력 / 현지화 en·ja 추가(이름·부제목은 app-name-subtitle.txt)
- [ ] 앱 개인정보 보호: **데이터 수집 안 함** → 게시 / 가격: 무료, 국가 전체
### ④ 버전 1.0 페이지 (ko/en/ja 전환하며 입력)
- [ ] 스크린샷: iPhone 6.9″ = `framed-iphone-6.9/<언어>/01~09` / Apple Watch = `framed-watch/<언어>/01~03`
- [ ] 프로모션 텍스트 / 설명 / 키워드 / 새로운 기능(whats-new-1.0) / 지원 URL
- [ ] App Review 정보: 로그인 필요 없음 + 연락처 + **메모(review-notes.txt 영문 그대로 — HealthKit·백그라운드 오디오·워치 설명)**
- [ ] 출시 방식: 자동 또는 수동
### ⑤ 아카이브 → 업로드 → TestFlight (⏳ 처리 10~60분)
- [ ] 세 타깃 버전·빌드 일치(1.0/1) 확인 → 스킴 Keging + Any iOS Device → Archive → Distribute → Upload
- [ ] 수출 규정 질문에 '아니요'(표준 암호화만)
- [ ] TestFlight 내부 테스트: 진동 3종·완료 진동·백그라운드 진동·DI/잠금화면·워치 기록 합류·건강 기록
### ⑥ 심사 제출 (⏳ 보통 24~48시간)
- [ ] 빌드 선택 → 제출. 거절 시 해결 센터 — 예상 쟁점: **백그라운드 오디오 모드(2.5.4)** 용도 소명(review-notes 메모), HealthKit 용도 문구(Info.plist 3언어 등록됨)
### ⑦ 승인 → 출시 → 직후
- [ ] 스토어 다운로드로 첫 실행·워치 앱 설치·건강 기록 확인
- [ ] CLAUDE.md §1 현재 상태·§8에 출시 완료 반영

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""워치 시뮬레이터 상태바의 빨간 '아이폰 연결 끊김' 아이콘 제거 (Keging 폴더에서 `python3 Marketing/tools/clean_watch_statusbar.py <png...>`).
워치 시뮬은 HKWorkoutSession/런타임 세션이 시작되면 상태바에 '연결 끊김' 아이콘을 붙인다(실기기는 실제로 끊겼을 때만).
`simctl status_bar` 오버라이드가 워치엔 없으므로, 우상단 상태바 영역의 빨간 픽셀 bbox를 주변 배경색으로 덮는다.
화면 내용은 건드리지 않는다 상태바 아이콘만.
"""
import sys
from PIL import Image
def clean(path: str) -> bool:
im = Image.open(path).convert("RGB")
w, h = im.size
x0, y0, x1, y1 = int(w * 0.5), int(h * 0.02), int(w * 0.98), int(h * 0.16)
px = im.load()
reds = [(x, y) for y in range(y0, y1) for x in range(x0, x1)
if px[x, y][0] > 150 and px[x, y][1] < 110 and px[x, y][2] < 110]
if not reds:
print(f"-- {path}: 아이콘 없음")
return False
xs = [p[0] for p in reds]; ys = [p[1] for p in reds]
bx0, by0, bx1, by1 = min(xs) - 3, min(ys) - 3, max(xs) + 3, max(ys) + 3
# 아이콘 왼쪽 여백의 배경색으로 채움 (상태바 배경 = 앱 배경)
fill = px[max(0, bx0 - 8), (by0 + by1) // 2]
for y in range(by0, by1 + 1):
for x in range(bx0, bx1 + 1):
px[x, y] = fill
im.save(path)
print(f"OK {path}: 아이콘 제거 bbox=({bx0},{by0})-({bx1},{by1}) fill={fill}")
return True
if __name__ == "__main__":
for p in sys.argv[1:]:
clean(p)

View File

@ -0,0 +1,290 @@
// App Store (macOS `swift Marketing/tools/frame_screenshots.swift` Keging )
// Marketing/tools/frame_screenshots.swift ( 6.9 + , · ).
// (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: (ko , en/ja : =Squeeze/, =Relax/)
struct Copy { let headline: [String: String]; let subline: [String: String] }
let iphoneCopies: [String: Copy] = [
"01-main": Copy(
headline: ["ko": "누르면 시작, 진동이 이끌어요", "en": "Tap Start, follow the buzz", "ja": "タップして開始、あとは振動におまかせ"],
subline: ["ko": "케겔 운동 타이머 — 광고도 결제도 없는 완전 무료", "en": "A Kegel timer — completely free, no ads", "ja": "ケーゲル体操タイマー — 広告も課金もない完全無料"]),
"02-workout": Copy(
headline: ["ko": "원이 오므라들면 조이고", "en": "Squeeze as the circle shrinks", "ja": "円が縮んだら締める"],
subline: ["ko": "수축이 시작될 때 긴 진동이 알려 줘요", "en": "A long buzz marks the start of every squeeze", "ja": "締める始まりを長い振動で知らせます"]),
"03-relax": Copy(
headline: ["ko": "부풀어 오르면 힘을 풀어요", "en": "Let go as it swells", "ja": "ふくらんだら力を抜く"],
subline: ["ko": "이완은 다른 진동 — 화면을 보지 않아도 돼요", "en": "Relax gets its own buzz — no need to look", "ja": "ゆるめるは別の振動 — 画面を見なくても大丈夫"]),
"04-stats-day": Copy(
headline: ["ko": "오늘 언제 했는지 한눈에", "en": "Your day at a glance", "ja": "今日いつやったかひと目で"],
subline: ["ko": "24시간 타임라인과 오늘 기록", "en": "A 24-hour timeline and today's sessions", "ja": "24時間タイムラインと今日の記録"]),
"05-stats-week": Copy(
headline: ["ko": "꾸준함이 그래프로 보여요", "en": "See your streak take shape", "ja": "続けた分がグラフに"],
subline: ["ko": "주간·월간 일별 횟수와 세트·횟수 합계", "en": "Weekly and monthly reps by day, with totals", "ja": "週間・月間の日別回数とセット・回数の合計"]),
"06-setup": Copy(
headline: ["ko": "내 몸에 맞는 세트로", "en": "Sets tuned to you", "ja": "自分に合ったセットに"],
subline: ["ko": "준비·수축·이완 시간, 횟수, 진동 패턴 3종", "en": "Ready, squeeze and relax times, reps, 3 haptic patterns", "ja": "準備・締める・ゆるめる時間、回数、振動パターン3種"]),
"07-dynamic-island": Copy(
headline: ["ko": "앱을 나가도 흐르는 현황", "en": "Keeps going when you leave", "ja": "アプリを離れても進む状況"],
subline: ["ko": "다이나믹 아일랜드·잠금화면의 실시간 타임라인", "en": "A live timeline in the Dynamic Island and on the Lock Screen", "ja": "Dynamic Island・ロック画面のリアルタイムタイムライン"]),
"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-finished": Copy(
headline: ["ko": "끝까지 마치면 기록돼요", "en": "Finish the set, it's recorded", "ja": "最後までやれば記録に"],
subline: ["ko": "완료 진동과 함께 통계·애플 건강에 저장", "en": "A long buzz, then saved to stats and Apple Health", "ja": "完了の振動とともに統計・ヘルスケアへ保存"]),
]
/// (canvas 416×496 )
let watchCopies: [String: Copy] = [
"01-main": Copy(
headline: ["ko": "손목에서 바로 시작", "en": "Start from your wrist", "ja": "手首からすぐ開始"],
subline: ["ko": "진동도 워치에서 울려요", "en": "Haptics right on the watch", "ja": "振動もウォッチで"]),
"02-workout": Copy(
headline: ["ko": "수축·이완이 한 화면에", "en": "Phase, time and rep", "ja": "締める・ゆるめるをひと画面に"],
subline: ["ko": "남은 시간과 몇 번째인지까지", "en": "All on one screen", "ja": "残り時間と回数も"]),
"03-settings": Copy(
headline: ["ko": "워치 전용 세팅", "en": "Watch-only setup", "ja": "ウォッチ専用の設定"],
subline: ["ko": "왼쪽으로 쓸어넘겨 조절해요", "en": "Swipe left to adjust", "ja": "左にスワイプして調整"]),
]
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),
]
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: (canvas 416×496 + )
func renderWatch(shot: String, locale: String, copy: Copy) -> Bool {
let fm = FileManager.default
let inPath = "Marketing/Screenshots/watch/\(locale)/\(shot).png"
guard let image = NSImage(contentsOfFile: inPath) else {
print("!! 원본 없음(watch): \(inPath)"); return false
}
let outDir = "Marketing/Screenshots/framed-watch/\(locale)"
let outPath = "\(outDir)/\(shot).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(copy.headline[locale] ?? "", weight: .bold, size: 32,
color: headlineColor, maxWidth: maxTextWidth)
let sub = fitted(copy.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 * (image.size.height / image.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()
image.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 (shot, copy) in watchCopies.sorted(by: { $0.key < $1.key }) {
for locale in locales {
if !renderWatch(shot: shot, locale: locale, copy: copy) { failures += 1 }
}
}
print(failures == 0 ? "== 전부 완료 ==" : "== 실패 \(failures)건 ==")
exit(failures == 0 ? 0 : 1)