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:
songyc macbook 2026-08-11 18:36:44 +09:00
parent e4fcf21b27
commit 62fa1a2d68
3 changed files with 60 additions and 1 deletions

View File

@ -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 표시)
---

View File

@ -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
}

View File

@ -6182,6 +6182,22 @@
}
}
},
"목표별 다짐": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Quests per Goal"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "目標別クエスト"
}
}
}
},
"목표에 속한 다짐들의 진행률을 원형 링으로 한눈에 봐요. (프리미엄)": {
"localizations": {
"en": {