diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index 38872a1..b9a3edd 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -259,9 +259,9 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}" ## 12. 시리 단축어 (`Shared/HaruDanimIntents.swift`, 프리미엄 게이트) -인텐트: 측정 시작/종료(짧은 기록 무시 규칙 동일 적용, **LiveActivityIntent 채택** — 시리 백그라운드 실행에서도 다이나믹 아일랜드 시작 허용) · 횟수 추가(1~999) · 행동 누적값 조회 · 목표 진행률 조회 · 다짐 진행률 조회(atMost는 한도 문구). `RunActionIntent`(위젯 버튼 전용)는 `isDiscoverable=false`로 단축어 앱에서 숨김이며 확장 프로세스 응답성 유지를 위해 LiveActivityIntent를 채택하지 않는다. +인텐트: 측정 시작/종료(짧은 기록 무시 규칙 동일 적용) · 횟수 추가(1~999) · 행동 누적값 조회 · 목표 진행률 조회 · 다짐 진행률 조회(atMost는 한도 문구). 시작/종료와 `RunActionIntent`(위젯 버튼)는 **LiveActivityIntent 채택** — 앱 프로세스에서 실행되어 시리·위젯 탭의 백그라운드 실행에서도 다이나믹 아일랜드가 바로 뜬다 (위젯 버튼 응답성 회귀 시 RunActionIntent의 채택만 되돌릴 것). `RunActionIntent`는 `isDiscoverable=false`로 단축어 앱에서 숨김. - **시리 문구**: `AppShortcutsProvider`에 기본 + 행동 이름 파라미터 문구("하루 다님에서 \(\.$action) 측정 시작" — 되묻지 않고 한 번에 실행). 번역은 **`IOS/AppShortcuts.xcstrings`**(일반 카탈로그와 별개, ko/en/ja). 행동 어휘는 앱 시작 시 `updateAppShortcutParameters()`로 갱신(새 행동은 다음 실행부터 음성 인식). -- 엔티티(Action/Goal/Quest)는 EntityStringQuery + '선택 안 함' 항목(위젯 설정용 센티널이 단축어 앱 선택지에도 노출됨 — 알려진 트레이드오프). 도움말 프리미엄 그룹에 사용법 항목, 결제 화면 기능 목록에도 표기. 로직 검증은 `-intentSmokeTest` (§14) — perform() 9개 시나리오를 앱 안에서 실행하고 결과를 Documents/intent-smoke-test.txt로 남긴다. +- 엔티티(Action/Goal/Quest)는 EntityStringQuery + '선택 안 함' 항목(위젯 설정용 센티널이 단축어 앱 선택지에도 노출됨 — 알려진 트레이드오프). 도움말 프리미엄 그룹에 사용법 항목, 결제 화면 기능 목록에도 표기. 로직 검증은 `-intentSmokeTest` (§14) — perform() 10개 시나리오를 앱 안에서 실행하고 결과를 Documents/intent-smoke-test.txt로 남긴다. --- @@ -289,7 +289,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}" | 모음 | `-startEditing` `-expandGoalCard` `-pinGoals ` `-openStatsFor "이름"` `-openHistoryFor "이름"` `-autoStart "이름"` | | 목표 | `-goalShowEditor` `-goalShowFinished` `-goalReorder` `-goalScrollBottom` `-endGoalYesterday "제목"`(종료일을 어제로 — 실행 시점 자동 판정 검증) | | 기록/통계 | `-historyMode timetable` `-historyWeekly` `-excludeActions "이름,이름"` `-historyShowFilter` `-statShowFilter` `-statSpan ` `-statScrollBottom` `-showExport` `-exportDump`(Documents/export-dump.png) | -| 설정/기타 | `-settingsScrollGoal` `-settingsScrollPremium` `-premiumPreview` `-helpPreview` `-widgetPreview YES|lock` `-widgetPreviewScroll <앵커>` `cloudSync`(standard bool로 강제) `-intentSmokeTest`(시리 인텐트 9개 시나리오 실행 → Documents/intent-smoke-test.txt, -seedDemo·-premium과 함께) | +| 설정/기타 | `-settingsScrollGoal` `-settingsScrollPremium` `-premiumPreview` `-helpPreview` `-widgetPreview YES|lock` `-widgetPreviewScroll <앵커>` `cloudSync`(standard bool로 강제) `-intentSmokeTest`(시리 인텐트 10개 시나리오 실행 → Documents/intent-smoke-test.txt, -seedDemo·-premium과 함께) | | 버블 | `-radialExpanded` | | 일기 | `-diarySeed` `-diaryOpenToday`(**`-startTab diary` 필수**) `-diaryPage ` `-diaryShowConfig` `-diaryShowExport`(달력 화면 onAppear — `-diaryOpenToday`와 함께 쓰면 안 뜸) `-diaryExportRun pdf|image`(결과를 Documents로 복사) `-diaryShowCalendarPicker` `-diarySeedEvents` `-diaryHideFirstAction` `-diaryShowActionFilter` / 섹션 강제: `-diary.sectionOrder "timetable,hero,goals"` `-diary.hiddenSections "mood,todos,records,bars"` | | 워치 | `-complicationPreview` `-complicationScroll` `-autoRunFirstAction` | diff --git a/myApp/HaruDanim/IOS/Core/DebugSeed.swift b/myApp/HaruDanim/IOS/Core/DebugSeed.swift index 68e2200..a6fd932 100644 --- a/myApp/HaruDanim/IOS/Core/DebugSeed.swift +++ b/myApp/HaruDanim/IOS/Core/DebugSeed.swift @@ -107,6 +107,15 @@ enum DebugSeed { log("타입 방어", "실패 — 시간형에 횟수 추가가 통과됨") } catch { log("타입 방어", "OK — 거부: \(String(localized: error.asIntentMessage))") } + // ⑤-2 위젯 실행 버튼(RunActionIntent): 시간형 시작 → 다시 실행하면 종료 토글 + do { + _ = try await RunActionIntent(actionID: running.id.uuidString).perform() + let startedOK = try IntentStore.action(running.id).isRunning + _ = try await RunActionIntent(actionID: running.id.uuidString).perform() + let stoppedOK = try !IntentStore.action(running.id).isRunning + log("위젯 실행 토글", startedOK && stoppedOK ? "OK — 시작 후 토글 종료" : "실패 — 시작=\(startedOK) 종료=\(stoppedOK)") + } catch { log("위젯 실행 토글", "throw: \(String(localized: error.asIntentMessage))") } + // ⑥ 조회 인텐트 3종 — throw 없이 완료되는지 do { let total = ActionTotalIntent() diff --git a/myApp/HaruDanim/Shared/HaruDanimIntents.swift b/myApp/HaruDanim/Shared/HaruDanimIntents.swift index d49d04d..5bd40d8 100644 --- a/myApp/HaruDanim/Shared/HaruDanimIntents.swift +++ b/myApp/HaruDanim/Shared/HaruDanimIntents.swift @@ -379,7 +379,10 @@ struct QuestEntityQuery: EntityStringQuery { // MARK: - 인텐트: 행동 실행 (위젯 버튼용 토글) /// 인터랙티브 위젯의 실행 버튼: 시간형은 시작/종료 토글, 횟수형은 +1 -struct RunActionIntent: AppIntent { +/// LiveActivityIntent 채택: 시스템이 앱 프로세스에서 실행해 위젯 탭으로 측정을 시작해도 +/// 다이나믹 아일랜드가 바로 뜬다. (과거 위젯 버튼 먹통 증상은 위젯 캐시/재부팅으로 해소된 +/// 것으로 확인되어 확장 프로세스 고정 제약을 풀었다 — 실기기에서 응답성 회귀 시 이 채택만 되돌릴 것) +struct RunActionIntent: LiveActivityIntent { static let title: LocalizedStringResource = "행동 실행" static let description = IntentDescription("시간 측정 행동은 시작/종료를 전환하고, 횟수 기록 행동은 횟수를 1 추가해요.") @@ -441,8 +444,7 @@ enum ActionRunner { // MARK: - 인텐트: 시간 측정 시작 /// LiveActivityIntent 채택: 시리/단축어의 백그라운드 실행에서도 Live Activity 시작이 -/// 허용되어, 앱을 열지 않아도 다이나믹 아일랜드에 측정이 표시된다. (앱 프로세스에서 실행됨 — -/// 위젯 버튼이 쓰는 RunActionIntent는 확장 프로세스 응답성 튜닝을 유지하려 채택하지 않는다) +/// 허용되어, 앱을 열지 않아도 다이나믹 아일랜드에 측정이 표시된다. (앱 프로세스에서 실행됨) struct StartTimeActionIntent: LiveActivityIntent { static let title: LocalizedStringResource = "시간 측정 시작" static let description = IntentDescription("시간 측정 행동의 측정을 시작해요.")