feat(report): 정리 리포트에 '목표별 다짐' 표 추가 — 검수 발견 C 해소
- 그룹=목표, 행=다짐(대상 아이콘·이름 + 주기 scheduleLabel[마감 시각 포함] + 목표량·방향 '3회 이상 달성'/'1시간 이하 유지') - 페이지 분할은 목표 그룹 단위(한 목표의 다짐이 페이지에 찢어지지 않음) - 신규 키 '목표별 다짐' en/ja 완역, 나머지는 기존 키 재사용 — 카탈로그 missing/stale 0 유지 - 벌크 1만 건(4페이지 스티치 1290×11042)·소량(iPad) 실물 검사 통과, Debug/Store 빌드 성공 - 검증 중 'Documents 산출물 0개' 소동은 macOS NFD 파일명 대 grep NFC 불일치 착시로 판명 — 코드 무결 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNxA172hNDRpDsJ2qztKPf
This commit is contained in:
parent
e4fcf21b27
commit
62fa1a2d68
@ -244,7 +244,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
- 테마(**시스템 설정 따름(기본)**/라이트/다크 — 즉시 적용, App Group 저장으로 위젯 공유. "system"은 스킴을 강제하지 않아 앱·위젯 모두 시스템 모드를 따름 — 2026-07-23 UX 감사에서 추가, 기본값도 light→system) / 언어(재시작 적용, AppleLanguages 오버라이드)
|
||||
- iPhone만: 내비게이션 방식(탭바/버블) + 탭바 구성 or 버블 순서·두 번 눌러 바로 이동(토글+대상 탭)
|
||||
- 모음 탭 목표 카드 선택(개수 무제한)+표시 방식 / 주 시작 요일 / 하루 시작 시간 / 짧은 기록 무시(**전 기기 공통 — iCloud 키-값 저장소 동기화**(1.1, `IOS/Core/SettingsCloudSync.swift` + `ubiquity-kvstore-identifier` 엔타이틀먼트): 검사는 종료를 실행한 기기의 설정 기준이라 기기별 값이면 교차 사용(아이패드 시작→아이폰 종료)에서 결과가 갈리던 실사용 보고의 해소. 값 저장은 여전히 App Group defaults고 KVS가 미러 — 시작 시 iCloud 값 채택(없으면 로컬 값 시드), 외부 변경·포그라운드 복귀 때 pull, 설정 변경 때 push(동일 값 가드로 되울림 방지). CloudKit 데이터 동기화(프리미엄 토글)와 별개라 iCloud 로그인만으로 동작, 미로그인이면 로컬 값 유지. 워치 종료는 페어링 아이폰에서 실행되므로 아이폰 설정 적용. 다른 Int 설정을 공통화하려면 syncedKeys에 추가만 하면 됨) / **장시간 측정 알림**(1~12시간, 켜는 순간 알림 권한 요청 — `SessionAlertManager`가 LiveActivityManager.sync 길목에서 진행 세션과 예약을 동기화, 세션 종료·설정 변경 시 예약 제거) / 대표 시간 기준(가장 먼저·나중에 시작 — 기본 latest)
|
||||
- 데이터: **데이터 내보내기** — 행동·시간·횟수·목표·다짐을 CSV 5파일로 (ISO 8601, UTF-8 BOM, 무료 포함. **1.4 구간 지정**: 전체(기본)/직접 지정 — 기록만 구간으로 거르고[세션은 겹침 포함] 행동·목표·다짐 목록은 항상 전체[가져오기 연결·백업 온전성], 구간 파일명 접미 -yyyyMMdd-yyyyMMdd. 생성 코어는 `IOS/Core/CSVExport.swift`로 추출 — 초기화 백업과 공유. 검증 `-dataExportRangeDays`) / **데이터 정리·초기화**(1.4, 무료 — `IOS/Core/DataReset.swift`+`ArchiveReport.swift`+`DataResetView`): 범위 전체/처음~지정 날짜(포함)까지. 범위 삭제는 기록·일기만(완전 포함 세션만 삭제 — 경계 걸침·진행 중 보존, 일기 dayKey≤키, 구조·양식 보존), 전체는 모든 도메인 데이터+LocalPrefs 6키(설정·프리미엄 보존). ⚠️컨텍스트 경유 개별 삭제만(스토어 파일 통삭제 금지 — CloudKit 전파). 삭제 전 백업 3종(CSV·일기 PDF·**정리 리포트**[ArchiveReport — 타이틀+기간(첫~마지막 기록일)+히어로+행동별 누적 표+꼬리표 합계+월별 표+목표 이력을 ExportPosterView 다페이지로, 24행 청크, PDF/스티치 PNG]) — 백업 켰으면 파일 생성 후에만 삭제 가능, 2단계 확인(다이얼로그→얼럿)+iCloud 전 기기·연속 끊김 경고. 검증 `-dataResetTest` 18건 / **데이터 가져오기**(1.1, 프리미엄 — `IOS/Core/CSVImport.swift`+`DataImportView`): 내보낸 CSV에서 행동·시간·횟수 기록 복원. **추가 전용**(수정·삭제 없음), 파일 선택(복수)→미리 보기→확인→저장 1회(DataChange.commit), **멱등**(기록 중복은 초 단위 절사 동일성으로 건너뜀 — 내보내기가 초 단위라 원본의 소수점 초 유사 중복까지 흡수), 파일 종류는 헤더가 아니라 **데이터 행 모양으로 판별**(UUID·ISO 날짜·time/count — 내보낸 언어와 무관), 행동 연결은 uuid 우선→이름+방식 폴백→새로 생성(csv uuid 보존, 꼬리표는 이름으로 재사용/프리셋 색 생성), '측정 중'(종료 없음) 행은 건너뜀(유령 Live Activity 방지), 목표·다짐은 비대상(다짐 CSV는 주기가 표시 문자열이라 재구성 불가 — 사용자 결정). 프리미엄 게이트 이유: CSV로 무료 한도(행동 10개) 우회 방지. 검증 `-csvImportTest`(§14)
|
||||
- 데이터: **데이터 내보내기** — 행동·시간·횟수·목표·다짐을 CSV 5파일로 (ISO 8601, UTF-8 BOM, 무료 포함. **1.4 구간 지정**: 전체(기본)/직접 지정 — 기록만 구간으로 거르고[세션은 겹침 포함] 행동·목표·다짐 목록은 항상 전체[가져오기 연결·백업 온전성], 구간 파일명 접미 -yyyyMMdd-yyyyMMdd. 생성 코어는 `IOS/Core/CSVExport.swift`로 추출 — 초기화 백업과 공유. 검증 `-dataExportRangeDays`) / **데이터 정리·초기화**(1.4, 무료 — `IOS/Core/DataReset.swift`+`ArchiveReport.swift`+`DataResetView`): 범위 전체/처음~지정 날짜(포함)까지. 범위 삭제는 기록·일기만(완전 포함 세션만 삭제 — 경계 걸침·진행 중 보존, 일기 dayKey≤키, 구조·양식 보존), 전체는 모든 도메인 데이터+LocalPrefs 6키(설정·프리미엄 보존). ⚠️컨텍스트 경유 개별 삭제만(스토어 파일 통삭제 금지 — CloudKit 전파). 삭제 전 백업 3종(CSV·일기 PDF·**정리 리포트**[ArchiveReport — 타이틀+기간(첫~마지막 기록일)+히어로+행동별 누적 표+꼬리표 합계+월별 표+목표 이력+**목표별 다짐 표**(그룹=목표, 대상·주기·목표량+방향 — 검수 후속으로 추가, 2026-08-11)를 ExportPosterView 다페이지로, 24행 청크(다짐은 목표 그룹 단위 분할), PDF/스티치 PNG]) — 백업 켰으면 파일 생성 후에만 삭제 가능, 2단계 확인(다이얼로그→얼럿)+iCloud 전 기기·연속 끊김 경고. 검증 `-dataResetTest` 18건 / **데이터 가져오기**(1.1, 프리미엄 — `IOS/Core/CSVImport.swift`+`DataImportView`): 내보낸 CSV에서 행동·시간·횟수 기록 복원. **추가 전용**(수정·삭제 없음), 파일 선택(복수)→미리 보기→확인→저장 1회(DataChange.commit), **멱등**(기록 중복은 초 단위 절사 동일성으로 건너뜀 — 내보내기가 초 단위라 원본의 소수점 초 유사 중복까지 흡수), 파일 종류는 헤더가 아니라 **데이터 행 모양으로 판별**(UUID·ISO 날짜·time/count — 내보낸 언어와 무관), 행동 연결은 uuid 우선→이름+방식 폴백→새로 생성(csv uuid 보존, 꼬리표는 이름으로 재사용/프리셋 색 생성), '측정 중'(종료 없음) 행은 건너뜀(유령 Live Activity 방지), 목표·다짐은 비대상(다짐 CSV는 주기가 표시 문자열이라 재구성 불가 — 사용자 결정). 프리미엄 게이트 이유: CSV로 무료 한도(행동 10개) 우회 방지. 검증 `-csvImportTest`(§14)
|
||||
- 프리미엄 화면(§8) / 도움말(`HelpView` — 기기별 주제 분기, DisclosureGroup. **그룹당 대표 주제 1개에 예시 스크린샷**(IOS/HelpImages/help-<이름>-<ko|en|ja>.jpg, 총 18장 ≈1.6MB) — 언어별 파일을 HelpImage 로더가 표시 언어로 선택(jpg라 UIImage(named:) 대신 URL 로드). 앱 화면·문구가 크게 바뀌면 해당 스크린샷도 재촬영할 것: 시뮬 로케일별 재시드 후 §14 인자로 촬영, 워치는 상태바 크롭) / 버전(번들 CFBundleShortVersionString 표시)
|
||||
|
||||
---
|
||||
|
||||
@ -313,6 +313,49 @@ enum ArchiveReport {
|
||||
))
|
||||
}
|
||||
|
||||
// 목표별 다짐 표 — CSV에만 있던 다짐 구성(대상·주기·목표량)을 리포트에도 담는다
|
||||
// (그룹 = 목표, 목표량 표기는 앱 용어 그대로 "3회 이상 달성"/"1시간 이하 유지")
|
||||
var questGroups: [ExportTableData.Group] = []
|
||||
for goal in goals {
|
||||
let rows = goal.sortedQuests.map { quest in
|
||||
ExportTableData.Row(
|
||||
symbol: quest.targetSymbol, color: quest.targetColor, name: quest.targetName,
|
||||
values: [quest.scheduleLabel,
|
||||
"\(quest.targetValueLabel) \(quest.direction.label)"]
|
||||
)
|
||||
}
|
||||
guard !rows.isEmpty else { continue }
|
||||
questGroups.append(.init(label: goal.title, rows: rows))
|
||||
}
|
||||
// 페이지당 rowsPerPage행 기준으로 '그룹(목표) 단위' 분할 — 한 목표의 다짐들이
|
||||
// 페이지에 걸쳐 찢어지지 않게 한다 (한 그룹이 자체로 넘치면 그 그룹만 한 페이지)
|
||||
var questPageGroups: [[ExportTableData.Group]] = []
|
||||
var currentGroups: [ExportTableData.Group] = []
|
||||
var currentRows = 0
|
||||
for group in questGroups {
|
||||
if !currentGroups.isEmpty && currentRows + group.rows.count > rowsPerPage {
|
||||
questPageGroups.append(currentGroups)
|
||||
currentGroups = []
|
||||
currentRows = 0
|
||||
}
|
||||
currentGroups.append(group)
|
||||
currentRows += group.rows.count
|
||||
}
|
||||
if !currentGroups.isEmpty { questPageGroups.append(currentGroups) }
|
||||
for (index, groups) in questPageGroups.enumerated() {
|
||||
let suffix = index == 0 ? "" : String(localized: " (계속)")
|
||||
let title = String(localized: "목표별 다짐") + suffix
|
||||
pages.append(ExportSnapshot(
|
||||
title: title, periodLabel: periodLabel, filterNote: nil,
|
||||
hero: [],
|
||||
sections: [.table(title: title, ExportTableData(
|
||||
columns: [String(localized: "주기"), String(localized: "목표량")],
|
||||
groups: groups
|
||||
))],
|
||||
fileSlug: slug
|
||||
))
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
|
||||
@ -6182,6 +6182,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"목표별 다짐": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Quests per Goal"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "目標別クエスト"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"목표에 속한 다짐들의 진행률을 원형 링으로 한눈에 봐요. (프리미엄)": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user