fix(sweep): full-app inspection fixes — watch command control reload, complication state key, bundle version, stale copy/catalog cleanup

전체 점검에서 발견한 저위험 문제들 일괄 수정:
- 워치 실행 명령 처리 경로에 ControlCenter.reloadAllControls() 추가
  (DataChange.commit과 동일 경로 유지 — 워치에서 시작/종료 시
  아이폰 제어 센터 토글이 낡은 상태로 남던 문제)
- 컴플리케이션 전용 전송 상태 키에 측정 시작 시각 포함
  (같은 행동을 종료 직후 디바운스 안에 재시작하면 상태 변화로
  인식하지 못해 컴플리케이션 타이머가 갱신되지 않던 엣지)
- 설정 탭 버전 표기를 하드코딩("1.0") 대신 번들
  CFBundleShortVersionString에서 읽도록 변경
- '대표 시간 기준' 설명의 낡은 문구 "(Live Activity 지원은 추후
  추가 예정)" 제거 — 실제 용처(다이나믹 아일랜드·잠금화면·워치
  컴플리케이션)로 갱신, en/ja 번역 포함
- 카탈로그 stale 항목 22개 정리 (IOS 20 + Widgets 2 — 과거 문구
  수정 때 남은 죽은 키), CLAUDE.md l10n 루틴에 stale 청소 단계 추가
- CLAUDE.md 남은 배포 작업에 코드 측 잔여 2건(약관 링크,
  ITSAppUsesNonExemptEncryption) 명시

검증: Debug·Store·워치 3스킴 빌드 성공, 전 카탈로그 missing 0,
인텐트 스모크 테스트 10개 통과, 스트릭 덤프 정상(시작일 하한 유지),
설정·모음 탭 스크린샷 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-14 13:42:54 +09:00
parent 8818578758
commit 86bdf3d2d7
5 changed files with 6971 additions and 7350 deletions

View File

@ -16,7 +16,7 @@
| 번들 ID | `com.yechan.HaruDanim` / App Group `group.com.yechan.HaruDanim` / CloudKit `iCloud.com.yechan.HaruDanim` | | 번들 ID | `com.yechan.HaruDanim` / App Group `group.com.yechan.HaruDanim` / CloudKit `iCloud.com.yechan.HaruDanim` |
| 수익 모델 | 부분 유료화. 무료 한도(행동 10·목표 2·목표당 다짐 3) + 프리미엄 결제 시 해제 | | 수익 모델 | 부분 유료화. 무료 한도(행동 10·목표 2·목표당 다짐 3) + 프리미엄 결제 시 해제 |
| 결제 상품 | `com.yechan.HaruDanim.premium.monthly`(월 5,000원) / `.yearly`(년 50,000원) / `.lifetime`(80,000원) — 임시 가격, App Store Connect 등록은 아직 안 됨 | | 결제 상품 | `com.yechan.HaruDanim.premium.monthly`(월 5,000원) / `.yearly`(년 50,000원) / `.lifetime`(80,000원) — 임시 가격, App Store Connect 등록은 아직 안 됨 |
| 남은 배포 작업 | App Store Connect 상품 3종 등록(구독 그룹 + 비소모성, 유료 앱 계약), 실기기 검증(펜슬 필기감·캘린더 권한), App Store 배포 | | 남은 배포 작업 | App Store Connect 상품 3종 등록(구독 그룹 + 비소모성, 유료 앱 계약), 실기기 검증(펜슬 필기감·캘린더 권한), App Store 배포. **코드 측 잔여 2건**: ① 결제 화면(PremiumView)에 개인정보처리방침·이용약관 링크(심사 3.1.2 필수 — URL 준비되면 추가), ② Info.plist `ITSAppUsesNonExemptEncryption=NO` 등록 |
--- ---
@ -68,6 +68,8 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}"
# 3) python으로 en/ja 번역 채움 → 4) sync 재실행으로 포맷 정규화 → 5) missing 확인 # 3) python으로 en/ja 번역 채움 → 4) sync 재실행으로 포맷 정규화 → 5) missing 확인
``` ```
문구를 **수정**했을 때는 옛 키가 `extractionState: stale`로 남는다 — sync 후 stale 항목을 삭제해 카탈로그를 청소할 것 (missing과 함께 stale도 확인).
⚠️ **카탈로그는 타깃별로 따로다** — Shared 파일의 문자열이라도 위젯·워치 타깃에서 쓰이면 Xcode가 빌드 시 그 타깃의 카탈로그(`Widgets/Localizable.xcstrings`, 워치 앱, `IOS/InfoPlist.xcstrings`)에도 추출한다. IOS 카탈로그만 채우면 위젯·워치 화면은 미번역으로 남으므로, 빌드 후 **모든 카탈로그의 missing을 확인**할 것. ⚠️ **카탈로그는 타깃별로 따로다** — Shared 파일의 문자열이라도 위젯·워치 타깃에서 쓰이면 Xcode가 빌드 시 그 타깃의 카탈로그(`Widgets/Localizable.xcstrings`, 워치 앱, `IOS/InfoPlist.xcstrings`)에도 추출한다. IOS 카탈로그만 채우면 위젯·워치 화면은 미번역으로 남으므로, 빌드 후 **모든 카탈로그의 missing을 확인**할 것.
용어 통일: 다짐=Quest/クエスト, 행동=Action, 꼬리표=Tag. `String(format:)`은 카탈로그로 추출되지 않으므로 사용자 노출 문구에 금지 (`Format.countAverage` 참고). 용어 통일: 다짐=Quest/クエスト, 행동=Action, 꼬리표=Tag. `String(format:)`은 카탈로그로 추출되지 않으므로 사용자 노출 문구에 금지 (`Format.countAverage` 참고).
@ -198,7 +200,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}"
- iPhone만: 내비게이션 방식(탭바/버블) + 탭바 구성 or 버블 순서·두 번 눌러 바로 이동(토글+대상 탭) - iPhone만: 내비게이션 방식(탭바/버블) + 탭바 구성 or 버블 순서·두 번 눌러 바로 이동(토글+대상 탭)
- 모음 탭 목표 카드 선택(개수 무제한)+표시 방식 / 주 시작 요일 / 하루 시작 시간 / 짧은 기록 무시 / **장시간 측정 알림**(1~12시간, 켜는 순간 알림 권한 요청 — `SessionAlertManager`가 LiveActivityManager.sync 길목에서 진행 세션과 예약을 동기화, 세션 종료·설정 변경 시 예약 제거) / 대표 시간 기준(가장 먼저·나중에 시작 — 기본 latest) - 모음 탭 목표 카드 선택(개수 무제한)+표시 방식 / 주 시작 요일 / 하루 시작 시간 / 짧은 기록 무시 / **장시간 측정 알림**(1~12시간, 켜는 순간 알림 권한 요청 — `SessionAlertManager`가 LiveActivityManager.sync 길목에서 진행 세션과 예약을 동기화, 세션 종료·설정 변경 시 예약 제거) / 대표 시간 기준(가장 먼저·나중에 시작 — 기본 latest)
- 데이터: **데이터 내보내기** — 행동·시간·횟수·목표·다짐을 CSV 5파일로 (ISO 8601, UTF-8 BOM, 무료 포함) - 데이터: **데이터 내보내기** — 행동·시간·횟수·목표·다짐을 CSV 5파일로 (ISO 8601, UTF-8 BOM, 무료 포함)
- 프리미엄 화면(§8) / 도움말(`HelpView` — 기기별 주제 분기, DisclosureGroup) / 버전 - 프리미엄 화면(§8) / 도움말(`HelpView` — 기기별 주제 분기, DisclosureGroup) / 버전(번들 CFBundleShortVersionString 표시)
--- ---

View File

@ -158,7 +158,10 @@ final class WatchSyncManager: NSObject {
/// 30 /// 30
private func pushForComplicationIfNeeded(session: WCSession, snapshot: WatchSnapshot, data: Data) { private func pushForComplicationIfNeeded(session: WCSession, snapshot: WatchSnapshot, data: Data) {
guard session.isComplicationEnabled else { return } guard session.isComplicationEnabled else { return }
let stateKey = "\(snapshot.runningName ?? "")|\(snapshot.extraRunningCount)|\(snapshot.isPremium)" // ( )
//
let startStamp = snapshot.runningStartAt.map { Int($0.timeIntervalSince1970) } ?? 0
let stateKey = "\(snapshot.runningName ?? "")|\(startStamp)|\(snapshot.extraRunningCount)|\(snapshot.isPremium)"
let now = Date.now let now = Date.now
guard stateKey != lastComplicationStateKey guard stateKey != lastComplicationStateKey
|| now.timeIntervalSince(lastComplicationPushAt) > 30 * 60 else { return } || now.timeIntervalSince(lastComplicationPushAt) > 30 * 60 else { return }
@ -183,6 +186,8 @@ final class WatchSyncManager: NSObject {
try? context.save() try? context.save()
LiveActivityManager.sync(context: context) LiveActivityManager.sync(context: context)
WidgetCenter.shared.reloadAllTimelines() WidgetCenter.shared.reloadAllTimelines()
// (DataChange.commit )
ControlCenter.shared.reloadAllControls()
} else { } else {
watchSyncLog.error("run command: action not found \(idString, privacy: .public)") watchSyncLog.error("run command: action not found \(idString, privacy: .public)")
} }

File diff suppressed because it is too large Load Diff

View File

@ -177,7 +177,7 @@ struct SettingsView: View {
} header: { } header: {
Text("다이나믹 아일랜드 · 잠금화면") Text("다이나믹 아일랜드 · 잠금화면")
} footer: { } footer: {
Text("여러 행동을 동시에 추적 중일 때 대표로 표시할 시간이에요. (Live Activity 지원은 추후 추가 예정)") Text("여러 행동을 동시에 추적 중일 때 다이나믹 아일랜드·잠금화면과 워치 컴플리케이션에 대표로 표시할 행동이에요.")
} }
Section("프리미엄") { Section("프리미엄") {
NavigationLink { NavigationLink {
@ -214,7 +214,9 @@ struct SettingsView: View {
HStack { HStack {
Text("버전") Text("버전")
Spacer() Spacer()
Text("1.0").foregroundStyle(.secondary) // MARKETING_VERSION
Text(verbatim: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0")
.foregroundStyle(.secondary)
} }
} }
} }

View File

@ -1317,29 +1317,6 @@
} }
} }
}, },
"매달 %lld째 주 %@요일": {
"extractionState": "stale",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Every month, week %lld on %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "毎月第%lld%@曜日"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "매달 %1$lld째 주 %2$@요일"
}
}
}
},
"매일": { "매일": {
"localizations": { "localizations": {
"en": { "en": {
@ -1404,23 +1381,6 @@
} }
} }
}, },
"몇째 주 요일": {
"extractionState": "stale",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Nth weekday"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "第何週の曜日"
}
}
}
},
"목": { "목": {
"comment": "요일 축약(목요일)", "comment": "요일 축약(목요일)",
"localizations": { "localizations": {