걸음 수 Health Connect·HealthKit · 위치 퀘스트 사진·QR 인증 · 행성 형태 확정

This commit is contained in:
Sn-Kinos
2026-10-05 23:57:01 +09:00
parent dc9861df38
commit dce2f64362
44 changed files with 930 additions and 66 deletions
@@ -8,9 +8,11 @@
/// 생성자로 다른 구현을 넣는다 — 센서 없는 환경에서도 옮기는 규칙을 검증할 수 있어야 한다.
library;
import 'dart:io' show Platform;
import 'dart:math' as math;
import 'package:geolocator/geolocator.dart';
import 'package:health/health.dart';
import 'package:pedometer/pedometer.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -308,6 +310,64 @@ Future<PlaceFix> devicePlaceFix() async {
/// 권한을 영구 거부한 사용자를 설정 화면으로 보낸다. 화면의 「설정 열기」가 부른다.
Future<void> openQuestPermissionSettings() => openAppSettings();
/// 오늘(KST 0시~지금) 걸은 수를 내는 함수. 쓸 수 없으면 null — 기기 센서([StepCountProbe])로 대체한다.
typedef TodayStepsProbe = Future<int?> Function();
/// 오늘 0시(KST) — 서버 `dayKey()` 와 같은 날짜 경계.
DateTime kstMidnight(DateTime at) {
final kst = at.toUtc().add(const Duration(hours: 9));
return DateTime.utc(kst.year, kst.month, kst.day).subtract(const Duration(hours: 9));
}
/// 권한 확인은 앱이 떠 있는 동안 한 번만 한다. 거부되면 다음 실행에서 다시 묻는다.
Future<bool>? _healthAuthorized;
Health? _health;
/// 오늘 걸음 — **Health Connect(Android) · HealthKit(iOS)** 의 그 날 0시(KST)부터 지금까지 합계.
///
/// 견적서 「만보기(Health API)」 · 별첨 1 ③-1 「Health 권한 동의 → 걸음 집계」 · 정책 원장 **B-26**
/// 「권한 거부 시 앱 자체 집계 전환」. 기기 걸음 센서는 앱을 연 뒤부터만 세므로(앱 열기 전 걸음 누락 —
/// 대조표 B-02) 이 값이 1차 경로이고, Health 를 쓸 수 없을 때(Health Connect 미설치 · 권한 거부 ·
/// HealthKit 미지원)만 null 을 내 센서 경로로 넘긴다. 판정은 여전히 서버가 한다.
///
/// 🟡 **임시값 — 회신 대기**(해석 확인 요청 영역B Q-B-09): 사용자가 건강 앱에 **직접 입력한 걸음은
/// 빼고** 센다(`includeManualEntry: false`). 자료에 정의가 없고, 넣으면 손으로 숫자를 적어 구간을
/// 받는다. Android 는 기록 앱이 입력 방식을 남기지 않으면 걸러지지 않는다(플러그인 문서).
///
/// ponytail: iOS 는 읽기 권한 거부를 알려 주지 않는다(HealthKit 개인정보 정책 — 거부해도 0 이 온다).
/// 그래서 iOS 에서 0 이면 센서 경로로 넘긴다. 실제로 0 걸음인 날은 센서도 0 이라 결과가 같다.
Future<int?> deviceTodaySteps() async {
try {
final health = _health ??= Health();
final ok = await (_healthAuthorized ??= () async {
await health.configure();
if (Platform.isAndroid &&
await health.getHealthConnectSdkStatus() != HealthConnectSdkStatus.sdkAvailable) {
return false;
}
const types = [HealthDataType.STEPS];
const access = [HealthDataAccess.READ];
return await health.hasPermissions(types, permissions: access) == true ||
await health.requestAuthorization(types, permissions: access);
}());
if (!ok) {
_healthAuthorized = null;
return null;
}
final now = DateTime.now();
final steps = await health.getTotalStepsInInterval(
kstMidnight(now),
now,
includeManualEntry: false,
);
if (steps == null || (Platform.isIOS && steps == 0)) return null;
return steps;
} catch (_) {
_healthAuthorized = null;
return null;
}
}
/// 오늘 걸은 수 — **하루 단위 기준선**.
///
/// 걸음 센서가 주는 값은 [deviceStepCount] 설명대로 **부팅 이후 누적**이다. 정책 원장